Compare commits

...

15 Commits

Author SHA1 Message Date
KodeStar 238452c104 Merge pull request #1583 from linuxserver/fix/app-icon-consistent-size
Fix inconsistent SVG tile icon sizes
2026-08-03 15:52:03 +01:00
KodeStar 91f7a2ec8d Fix inconsistent SVG tile icon sizes
SVG icons with explicit width/height attributes rendered tiny because
max-width/max-height only caps intrinsic size and never scales up, while
dimensionless SVGs defaulted large and were capped to 60px. Pin .app-icon
to a fixed 60x60 box with object-fit: contain so every icon renders at a
consistent size while preserving aspect ratio.

Fixes #1582
2026-08-03 15:47:36 +01:00
KodeStar 62823aef63 Merge pull request #1580 from tuxinaut/chore/de-language-update
🇩🇪 Update German translation
2026-07-10 17:10:02 +01:00
Schäfer, Denny 2ceec61e17 Update German translation 2026-07-10 12:30:14 +02:00
KodeStar 8fc5f3ccbf Merge pull request #1579 from linuxserver/release/v2.8.1
Tag and release / Tag and publish release (push) Has been cancelled
Bump version to 2.8.1
2026-07-09 22:58:46 +01:00
github-actions[bot] 6a45d44f69 Bump version to 2.8.1 2026-07-09 21:35:30 +00:00
KodeStar 327af60d1c Update CI 2026-07-09 22:33:11 +01:00
KodeStar a38ecd4864 Automate bumping version 2026-07-09 22:21:37 +01:00
KodeStar 1f7432fa26 Merge pull request #1578 from linuxserver/feat/truenas-websocket-client
Add WebSocket support for TrueNAS JSON-RPC 2.0 API
2026-07-09 22:06:47 +01:00
KodeStar 55bda9daf0 Merge pull request #1577 from linuxserver/fix/checkbox-test-config
Fix checkbox config values always returning "1" in Test config
2026-07-09 22:06:42 +01:00
KodeStar 667dd4c129 Merge pull request #1576 from linuxserver/feat/global-tls-skip
Add global TLS verification skip setting
2026-07-09 22:06:38 +01:00
KodeStar d16b7f60f3 Vendor phrity/websocket dependency and fix v3 exception namespace
The WebSocket support added for TrueNAS JSON-RPC 2.0 requires the
phrity/websocket library to actually be available at runtime. This repo
commits the vendor/ tree (CI does not run composer install), so the
dependency and composer.lock must be committed for class_exists() checks
in the TrueNAS app to succeed.

- composer require phrity/websocket:^3.6 (resolves to 3.7.3) with lock
  and vendor/ committed
- Fix TrueNASWebSocketClient to catch WebSocket\Exception\Exception
  (phrity/websocket v3 namespace) instead of the non-existent
  WebSocket\ConnectionException from the old textalk/websocket v1/v2 API,
  so connection/call failures are logged and wrapped as intended
2026-07-09 22:01:18 +01:00
Jay Collett 57fa9f26fb Fix checkbox config values always returning "1"
Checkbox-type config options (e.g. ignore_tls) always tested as "1"
regardless of the checkbox state. The Test-button config gatherer used
$(this).val() for every .config-item, and jQuery .val() on a checkbox
returns its value attribute ("1") regardless of checked state. App
config blades pair a hidden input (0) with a checkbox (1) sharing the
same data-config, and the checkbox is last in DOM order, so it always
overwrote the value with "1". Normal form saves were unaffected.

Use the :checked state for checkboxes so unchecking now tests as "0".

Also rebuilds the committed compiled bundle (public/js/app.js) so the
fix takes effect at runtime; the edit is byte-identical to the Laravel
Mix development build output for this block.

Fixes linuxserver/Heimdall-Apps#782
2026-07-09 21:59:46 +01:00
Jay Collett bdf9160fdc Add global TLS verification skip setting
Adds a new boolean setting in the Advanced settings group that allows
users to globally skip TLS certificate verification for all enhanced
apps. This is useful for users who have self-signed certificates on
their services.

When enabled, the Guzzle HTTP client will set 'verify' => false for
all API requests made by enhanced apps.

Resolves linuxserver/Heimdall-Apps#687
2026-07-09 21:57:45 +01:00
Jay Collett 93a321ff5b Add WebSocket support for TrueNAS JSON-RPC 2.0 API
TrueNAS is deprecating the REST API (api/v2.0/) in version 26.04,
requiring migration to JSON-RPC 2.0 over WebSocket.

This commit adds:
- phrity/websocket dependency for WebSocket communication
- TrueNASWebSocketClient helper class that handles:
  - Connection to ws(s)://host/api/current
  - Authentication via auth.login_with_api_key
  - JSON-RPC 2.0 request/response formatting
  - TLS verification toggle
  - Proper connection cleanup

Refs: #1530
2026-07-09 21:56:41 +01:00
157 changed files with 14382 additions and 10 deletions
+97
View File
@@ -0,0 +1,97 @@
name: Release
# Stage 1 of the release flow: opens a version-bump PR against 2.x.
# When that PR is merged, tag-release.yml (stage 2) creates the tag and
# the GitHub release automatically.
on:
workflow_dispatch:
inputs:
bump:
description: 'Version bump type'
required: true
default: 'patch'
type: choice
options:
- patch
- minor
- major
version:
description: 'Explicit version (e.g. 2.9.0) — overrides bump type'
required: false
type: string
permissions:
contents: write
pull-requests: write
jobs:
release-pr:
name: Open version bump PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: 2.x
fetch-depth: 0
# Optional: set a RELEASE_TOKEN repo secret (fine-grained PAT with
# contents + pull-requests write) so the bump PR triggers CI checks.
# PRs created with the default github.token do not trigger workflows.
token: ${{ secrets.RELEASE_TOKEN || github.token }}
- name: Determine new version
id: version
env:
EXPLICIT_VERSION: ${{ inputs.version }}
BUMP: ${{ inputs.bump }}
run: |
current=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
if [ -z "$current" ]; then
echo "::error::Could not read current version from config/app.php"
exit 1
fi
if [ -n "$EXPLICIT_VERSION" ]; then
new="$EXPLICIT_VERSION"
if ! echo "$new" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Invalid version '$new' — expected X.Y.Z"
exit 1
fi
else
IFS=. read -r major minor patch <<< "$current"
case "$BUMP" in
major) new="$((major + 1)).0.0" ;;
minor) new="$major.$((minor + 1)).0" ;;
patch) new="$major.$minor.$((patch + 1))" ;;
esac
fi
if git rev-parse -q --verify "refs/tags/v$new" > /dev/null; then
echo "::error::Tag v$new already exists"
exit 1
fi
echo "Bumping $current -> $new"
echo "new=$new" >> "$GITHUB_OUTPUT"
- name: Push bump commit to release branch
env:
NEW_VERSION: ${{ steps.version.outputs.new }}
run: |
sed -i -E "s/^([[:space:]]*'version' => ')[0-9.]+(',)/\1$NEW_VERSION\2/" config/app.php
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -am "Bump version to $NEW_VERSION"
git push --force origin "HEAD:refs/heads/release/v$NEW_VERSION"
- name: Open pull request
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || github.token }}
NEW_VERSION: ${{ steps.version.outputs.new }}
run: |
existing=$(gh pr list --head "release/v$NEW_VERSION" --base 2.x --state open --json number -q '.[0].number')
if [ -n "$existing" ]; then
echo "PR #$existing is already open for release/v$NEW_VERSION"
else
gh pr create \
--base 2.x \
--head "release/v$NEW_VERSION" \
--title "Bump version to $NEW_VERSION" \
--body "Automated version bump. Merging this PR will tag v$NEW_VERSION and publish the GitHub release. Merge it last, once everything for the release is on 2.x."
fi
+41
View File
@@ -0,0 +1,41 @@
name: Tag and release
# Stage 2 of the release flow: whenever the version in config/app.php changes
# on 2.x (normally by merging the PR opened by release.yml, but a hand-made
# bump PR works too), create the matching tag and GitHub release.
on:
push:
branches:
- 2.x
paths:
- config/app.php
permissions:
contents: write
jobs:
tag-release:
name: Tag and publish release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create release if version is untagged
env:
GH_TOKEN: ${{ github.token }}
run: |
version=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
if [ -z "$version" ]; then
echo "::error::Could not read version from config/app.php"
exit 1
fi
if git rev-parse -q --verify "refs/tags/v$version" > /dev/null; then
echo "Tag v$version already exists — nothing to do"
exit 0
fi
gh release create "v$version" \
--target "$GITHUB_SHA" \
--title "v$version" \
--generate-notes
+26
View File
@@ -0,0 +1,26 @@
name: Tag version check
# Safety net for manually pushed tags: fails if the tag doesn't match the
# version in config/app.php. Tags created by tag-release.yml use GITHUB_TOKEN
# and therefore don't trigger this (they always match anyway).
on:
push:
tags:
- 'v*'
jobs:
check:
name: Tag matches config/app.php
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Compare tag with app version
run: |
tag="${GITHUB_REF_NAME#v}"
version=$(sed -nE "s/^[[:space:]]*'version' => '([0-9]+\.[0-9]+\.[0-9]+)',/\1/p" config/app.php)
if [ "$tag" != "$version" ]; then
echo "::error::Tag v$tag does not match config/app.php version $version — bump the version (or use the Release workflow, which does it for you)"
exit 1
fi
echo "Tag v$tag matches config/app.php"
+187
View File
@@ -0,0 +1,187 @@
<?php
namespace App\Helpers;
use Illuminate\Support\Facades\Log;
use Phrity\Net\Context;
use WebSocket\Client;
use WebSocket\Exception\Exception as WebSocketException;
/**
* TrueNAS JSON-RPC 2.0 WebSocket Client
*
* Handles WebSocket communication with TrueNAS using the JSON-RPC 2.0 protocol.
* Required for TrueNAS 25.04+ as the REST API is deprecated.
*
* @see https://api.truenas.com/v25.10/jsonrpc.html
*/
class TrueNASWebSocketClient
{
private ?Client $client = null;
private string $url;
private string $apiKey;
private bool $ignoreTls;
private bool $authenticated = false;
private int $requestId = 1;
/**
* Create a new TrueNAS WebSocket client instance.
*
* @param string $baseUrl The base URL of the TrueNAS instance (e.g., https://truenas.local)
* @param string $apiKey The API key for authentication
* @param bool $ignoreTls Whether to skip TLS certificate verification
*/
public function __construct(string $baseUrl, string $apiKey, bool $ignoreTls = false)
{
$baseUrl = rtrim($baseUrl, '/');
$scheme = parse_url($baseUrl, PHP_URL_SCHEME);
$host = parse_url($baseUrl, PHP_URL_HOST);
$port = parse_url($baseUrl, PHP_URL_PORT);
$wsScheme = ($scheme === 'https') ? 'wss' : 'ws';
$portPart = $port ? ':' . $port : '';
$this->url = "{$wsScheme}://{$host}{$portPart}/api/current";
$this->apiKey = $apiKey;
$this->ignoreTls = $ignoreTls;
}
/**
* Connect to the TrueNAS WebSocket API and authenticate.
*
* @return bool True if connection and authentication succeeded
* @throws \Exception If connection or authentication fails
*/
public function connect(): bool
{
if ($this->client !== null && $this->authenticated) {
return true;
}
// Build SSL options - always force HTTP/1.1 via ALPN for WebSocket compatibility
// TrueNAS nginx defaults to HTTP/2 which doesn't support WebSocket upgrade
$sslOptions = [
'alpn_protocols' => 'http/1.1',
];
if ($this->ignoreTls) {
$sslOptions['verify_peer'] = false;
$sslOptions['verify_peer_name'] = false;
$sslOptions['allow_self_signed'] = true;
}
// Create context using phrity/net-stream Context class (required by phrity/websocket v3.x)
$streamContext = stream_context_create(['ssl' => $sslOptions]);
$context = new Context($streamContext);
try {
$this->client = new Client($this->url);
$this->client->setTimeout(15);
$this->client->setContext($context);
$authResult = $this->call('auth.login_with_api_key', [$this->apiKey]);
if ($authResult === true) {
$this->authenticated = true;
return true;
}
throw new \Exception('Authentication failed: Invalid API key');
} catch (WebSocketException $e) {
Log::error('TrueNAS WebSocket connection failed: ' . $e->getMessage());
$this->disconnect();
throw new \Exception('WebSocket connection failed: ' . $e->getMessage());
}
}
/**
* Make a JSON-RPC 2.0 call to the TrueNAS API.
*
* @param string $method The JSON-RPC method name (e.g., 'system.info')
* @param array $params Optional parameters for the method
* @return mixed The result from the API call
* @throws \Exception If the call fails or returns an error
*/
public function call(string $method, array $params = [])
{
if ($this->client === null) {
throw new \Exception('WebSocket client not connected');
}
$request = [
'jsonrpc' => '2.0',
'method' => $method,
'id' => $this->requestId++,
];
if (!empty($params)) {
$request['params'] = $params;
}
try {
$this->client->text(json_encode($request));
$response = $this->client->receive();
$decoded = json_decode($response->getContent(), true);
if (isset($decoded['error'])) {
$errorMsg = $decoded['error']['message'] ?? 'Unknown error';
$errorCode = $decoded['error']['code'] ?? 0;
throw new \Exception("API error ({$errorCode}): {$errorMsg}");
}
return $decoded['result'] ?? null;
} catch (WebSocketException $e) {
Log::error('TrueNAS WebSocket call failed: ' . $e->getMessage());
throw new \Exception('WebSocket call failed: ' . $e->getMessage());
}
}
/**
* Close the WebSocket connection.
*/
public function disconnect(): void
{
if ($this->client !== null) {
try {
$this->client->close();
} catch (\Exception $e) {
Log::debug('Error closing WebSocket: ' . $e->getMessage());
}
$this->client = null;
$this->authenticated = false;
}
}
/**
* Check if the client is connected and authenticated.
*
* @return bool
*/
public function isConnected(): bool
{
return $this->client !== null && $this->authenticated;
}
/**
* Test the connection by calling core.ping.
*
* @return bool True if the ping succeeds
*/
public function ping(): bool
{
try {
$result = $this->call('core.ping');
return $result === 'pong';
} catch (\Exception $e) {
return false;
}
}
/**
* Clean up on destruction.
*/
public function __destruct()
{
$this->disconnect();
}
}
+5
View File
@@ -85,6 +85,11 @@ abstract class SupportedApps
'connect_timeout' => 15, 'connect_timeout' => 15,
] : $overridevars; ] : $overridevars;
// Check global setting to skip TLS verification (useful for self-signed certificates)
if (Setting::fetch('skip_tls_verification')) {
$vars['verify'] = false;
}
$client = new Client($vars); $client = new Client($vars);
$method = ($overridemethod === null || $overridemethod === false) ? $this->method : $overridemethod; $method = ($overridemethod === null || $overridemethod === false) ? $this->method : $overridemethod;
+1
View File
@@ -19,6 +19,7 @@
"laravel/ui": "^4.4", "laravel/ui": "^4.4",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"nunomaduro/collision": "^8.0", "nunomaduro/collision": "^8.0",
"phrity/websocket": "^3.6",
"spatie/laravel-html": "^3.11", "spatie/laravel-html": "^3.11",
"spatie/laravel-ignition": "^2.4", "spatie/laravel-ignition": "^2.4",
"symfony/yaml": "^7.0" "symfony/yaml": "^7.0"
Generated
+599 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "24b785f73e421451022a5e23d68cc6f0", "content-hash": "90b6ed264d7efa958636840d86196b99",
"packages": [ "packages": [
{ {
"name": "aws/aws-crt-php", "name": "aws/aws-crt-php",
@@ -3536,6 +3536,84 @@
], ],
"time": "2026-02-16T23:10:27+00:00" "time": "2026-02-16T23:10:27+00:00"
}, },
{
"name": "nyholm/psr7",
"version": "1.8.2",
"source": {
"type": "git",
"url": "https://github.com/Nyholm/psr7.git",
"reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
"reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
"shasum": ""
},
"require": {
"php": ">=7.2",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"provide": {
"php-http/message-factory-implementation": "1.0",
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"http-interop/http-factory-tests": "^0.9",
"php-http/message-factory": "^1.0",
"php-http/psr7-integration-tests": "^1.0",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
"symfony/error-handler": "^4.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"autoload": {
"psr-4": {
"Nyholm\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
},
{
"name": "Martijn van der Ven",
"email": "martijn@vanderven.se"
}
],
"description": "A fast PHP7 implementation of PSR-7",
"homepage": "https://tnyholm.se",
"keywords": [
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/Nyholm/psr7/issues",
"source": "https://github.com/Nyholm/psr7/tree/1.8.2"
},
"funding": [
{
"url": "https://github.com/Zegnat",
"type": "github"
},
{
"url": "https://github.com/nyholm",
"type": "github"
}
],
"time": "2024-09-09T07:06:30+00:00"
},
{ {
"name": "php-http/cache-plugin", "name": "php-http/cache-plugin",
"version": "2.1.0", "version": "2.1.0",
@@ -4046,6 +4124,526 @@
], ],
"time": "2025-12-27T19:41:33+00:00" "time": "2025-12-27T19:41:33+00:00"
}, },
{
"name": "phrity/comparison",
"version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-comparison.git",
"reference": "cf80abb822537eeaaeb4142157cd667ca6372a13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-comparison/zipball/cf80abb822537eeaaeb4142157cd667ca6372a13",
"reference": "cf80abb822537eeaaeb4142157cd667ca6372a13",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Comparison\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.",
"homepage": "https://phrity.sirn.se/comparison",
"keywords": [
"comparable",
"comparator",
"comparison",
"equalable",
"filter",
"sort"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-comparison/issues",
"source": "https://github.com/sirn-se/phrity-comparison/tree/1.4.1"
},
"time": "2025-12-05T07:38:30+00:00"
},
{
"name": "phrity/http",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-http.git",
"reference": "1e7eee67359287b94aae2b7d40b730d5f5394943"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-http/zipball/1e7eee67359287b94aae2b7d40b730d5f5394943",
"reference": "1e7eee67359287b94aae2b7d40b730d5f5394943",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"guzzlehttp/psr7": "^2.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Http\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Utilities and interfaces for handling HTTP.",
"homepage": "https://phrity.sirn.se/http",
"keywords": [
"HTTP Factories",
"HTTP Serializer",
"http",
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-http/issues",
"source": "https://github.com/sirn-se/phrity-http/tree/1.1.0"
},
"time": "2025-12-22T20:22:29+00:00"
},
{
"name": "phrity/net-stream",
"version": "2.4.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-net-stream.git",
"reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/a0726a8dddf11a4cd3a9e45d17e145d78e948f43",
"reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-errorhandler": "^1.1",
"phrity/util-interpolator": "^1.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/net-uri": "^2.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory",
"homepage": "https://phrity.sirn.se/net-stream",
"keywords": [
"Socket",
"client",
"psr-17",
"psr-7",
"server",
"stream",
"stream factory"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-net-stream/issues",
"source": "https://github.com/sirn-se/phrity-net-stream/tree/2.4.0"
},
"time": "2026-03-25T18:16:40+00:00"
},
{
"name": "phrity/net-uri",
"version": "2.2.1",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-net-uri.git",
"reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/0737de026b75177ae302ac9fdbbd0ffc2610f3b8",
"reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.1",
"phrity/comparison": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 | ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"suggest": {
"ext-intl": "Enables IDN conversion for non-ASCII domains"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "PSR-7 Uri and PSR-17 UriFactory implementation",
"homepage": "https://phrity.sirn.se/net-uri",
"keywords": [
"psr-17",
"psr-7",
"uri",
"uri factory"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-net-uri/issues",
"source": "https://github.com/sirn-se/phrity-net-uri/tree/2.2.1"
},
"time": "2025-12-05T10:39:22+00:00"
},
{
"name": "phrity/util-accessor",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-accessor.git",
"reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-accessor/zipball/e9741da6b532fa2da7f627fce60fa77f00f6e354",
"reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-transformer": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.",
"homepage": "https://phrity.sirn.se/util-accessor",
"keywords": [
"accessor"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-accessor/issues",
"source": "https://github.com/sirn-se/phrity-util-accessor/tree/1.3.1"
},
"time": "2025-12-05T21:18:15+00:00"
},
{
"name": "phrity/util-errorhandler",
"version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
"reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/70a669cc22db2eed6a109ec66fd95168a4332c9b",
"reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Inline error handler; catch and resolve errors for code block.",
"homepage": "https://phrity.sirn.se/util-errorhandler",
"keywords": [
"error",
"warning"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
"source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.2.2"
},
"time": "2025-12-05T21:25:36+00:00"
},
{
"name": "phrity/util-interpolator",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-interpolator.git",
"reference": "18b0362d2aff60984c530808333c5e64c80c3e50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-interpolator/zipball/18b0362d2aff60984c530808333c5e64c80c3e50",
"reference": "18b0362d2aff60984c530808333c5e64c80c3e50",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-accessor": "^1.3",
"phrity/util-transformer": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Util\\Interpolator\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Interpolator class and trait.",
"homepage": "https://phrity.sirn.se/util-interpolator",
"keywords": [
"interpolate",
"interpolator"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-interpolator/issues",
"source": "https://github.com/sirn-se/phrity-util-interpolator/tree/1.1.1"
},
"time": "2025-12-05T21:35:25+00:00"
},
{
"name": "phrity/util-transformer",
"version": "1.3.1",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-transformer.git",
"reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-transformer/zipball/3fd4d7fa4077deafe5130c4b9ee8146b5488abe3",
"reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0",
"symfony/property-access": "^6.0 || ^7.0 || ^8.0",
"symfony/serializer": "^6.0 || ^7.0 || ^8.0",
"symfony/translation-contracts": "^3.0",
"symfony/uid": "^6.0 || ^7.0 || ^8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Phrity\\Util\\Transformer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Type transformers, normalizers and resolvers.",
"homepage": "https://phrity.sirn.se/util-transformer",
"keywords": [
"normalizer",
"resolver",
"transformer",
"type"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-transformer/issues",
"source": "https://github.com/sirn-se/phrity-util-transformer/tree/1.3.1"
},
"time": "2025-12-05T22:07:04+00:00"
},
{
"name": "phrity/websocket",
"version": "3.7.3",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/websocket-php.git",
"reference": "c92d613ec298ea108b8ebae306609cd40427f7d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/c92d613ec298ea108b8ebae306609cd40427f7d6",
"reference": "c92d613ec298ea108b8ebae306609cd40427f7d6",
"shasum": ""
},
"require": {
"nyholm/psr7": "^1.4",
"php": "^8.1",
"phrity/http": "^1.1",
"phrity/net-stream": "^2.4",
"phrity/net-uri": "^2.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/logger-console": "^1.0",
"phrity/net-mock": "^2.4",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
},
"type": "library",
"autoload": {
"psr-4": {
"WebSocket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "WebSocket client and server",
"homepage": "https://phrity.sirn.se/websocket",
"keywords": [
"client",
"server",
"websocket"
],
"support": {
"issues": "https://github.com/sirn-se/websocket-php/issues",
"source": "https://github.com/sirn-se/websocket-php/tree/3.7.3"
},
"time": "2026-06-22T14:53:45+00:00"
},
{ {
"name": "psr/cache", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",
+1 -1
View File
@@ -5,7 +5,7 @@ use Illuminate\Support\Facades\Facade;
return [ return [
'version' => '2.8.0', 'version' => '2.8.1',
'appsource' => env('APP_SOURCE', 'https://appslist.heimdall.site/'), 'appsource' => env('APP_SOURCE', 'https://appslist.heimdall.site/'),
+14
View File
@@ -364,5 +364,19 @@ class SettingsSeeder extends Seeder
$setting->label = 'app.settings.default_tag'; $setting->label = 'app.settings.default_tag';
$setting->save(); $setting->save();
} }
if (! $setting = Setting::find(16)) {
$setting = new Setting;
$setting->id = 16;
$setting->group_id = 4;
$setting->key = 'skip_tls_verification';
$setting->type = 'boolean';
$setting->label = 'app.settings.skip_tls_verification';
$setting->value = '0';
$setting->save();
} else {
$setting->label = 'app.settings.skip_tls_verification';
$setting->save();
}
} }
} }
+11
View File
@@ -28,6 +28,12 @@ return array (
'settings.view' => 'Ansicht', 'settings.view' => 'Ansicht',
'settings.custom_css' => 'Angepasstes CSS', 'settings.custom_css' => 'Angepasstes CSS',
'settings.custom_js' => 'Angepasstes JavaScript', 'settings.custom_js' => 'Angepasstes JavaScript',
'settings.treat_tags_as' => 'Tags behandeln als:',
'settings.default_tag' => 'Standard Tag',
'settings.folders' => 'Ordner',
'settings.tags' => 'Tags',
'settings.categories' => 'Kategorien',
'settings.skip_tls_verification' => 'TLS-Überprüfung überspringen (selbstsignierte Zertifikate)',
'options.none' => '- nicht festgelegt -', 'options.none' => '- nicht festgelegt -',
'options.google' => 'Google', 'options.google' => 'Google',
'options.ddg' => 'DuckDuckGo', 'options.ddg' => 'DuckDuckGo',
@@ -74,6 +80,7 @@ return array (
'apps.only_admin_account' => 'Nur mit Admin-Konto!', 'apps.only_admin_account' => 'Nur mit Admin-Konto!',
'apps.autologin_url' => 'Auto Login URL', 'apps.autologin_url' => 'Auto Login URL',
'apps.show_deleted' => 'Gelöschte Anwendung anzeigen', 'apps.show_deleted' => 'Gelöschte Anwendung anzeigen',
'app.import' => 'Importieren',
'dashboard' => 'Home Dashboard', 'dashboard' => 'Home Dashboard',
'user.user_list' => 'Nutzer', 'user.user_list' => 'Nutzer',
'user.add_user' => 'Nutzer hinzufügen', 'user.add_user' => 'Nutzer hinzufügen',
@@ -88,6 +95,8 @@ return array (
'delete' => 'Löschen', 'delete' => 'Löschen',
'optional' => 'Optional', 'optional' => 'Optional',
'restore' => 'Wiederherstellen', 'restore' => 'Wiederherstellen',
'export' => 'Exportieren',
'import' => 'Importieren',
'alert.success.item_created' => 'Element erfolgreich erstellt', 'alert.success.item_created' => 'Element erfolgreich erstellt',
'alert.success.item_updated' => 'Element erfolgreich aktualisiert', 'alert.success.item_updated' => 'Element erfolgreich aktualisiert',
'alert.success.item_deleted' => 'Element erfolgreich gelöscht', 'alert.success.item_deleted' => 'Element erfolgreich gelöscht',
@@ -99,6 +108,8 @@ return array (
'alert.success.tag_restored' => 'Tag erfolgreich wiederhergestellt', 'alert.success.tag_restored' => 'Tag erfolgreich wiederhergestellt',
'alert.success.setting_updated' => 'Die Einstellungen wurden übernommen', 'alert.success.setting_updated' => 'Die Einstellungen wurden übernommen',
'alert.error.not_exist' => 'Diese Einstellung existiert nicht.', 'alert.error.not_exist' => 'Diese Einstellung existiert nicht.',
'alert.error.file_too_big' => 'Datei zu groß',
'alert.error.file_not_stored' => 'Datei konnte nicht gespeichert werden',
'alert.success.user_created' => 'Nutzer erfolgreich erstellt', 'alert.success.user_created' => 'Nutzer erfolgreich erstellt',
'alert.success.user_updated' => 'Nutzer erfolgreich aktualisiert', 'alert.success.user_updated' => 'Nutzer erfolgreich aktualisiert',
'alert.success.user_deleted' => 'Nutzer erfolgreich gelöscht', 'alert.success.user_deleted' => 'Nutzer erfolgreich gelöscht',
+1
View File
@@ -33,6 +33,7 @@ return array (
'settings.folders' => 'Folders', 'settings.folders' => 'Folders',
'settings.tags' => 'Tags', 'settings.tags' => 'Tags',
'settings.categories' => 'Categories', 'settings.categories' => 'Categories',
'settings.skip_tls_verification' => 'Skip TLS Verification (for self-signed certificates)',
'options.none' => '- not set -', 'options.none' => '- not set -',
'options.google' => 'Google', 'options.google' => 'Google',
'options.ddg' => 'DuckDuckGo', 'options.ddg' => 'DuckDuckGo',
+3 -2
View File
@@ -927,9 +927,10 @@ div.create .input input, div.create .input select {
} }
.app-icon { .app-icon {
max-width: 60px; width: 60px;
height: 60px;
object-fit: contain;
display: block; display: block;
max-height: 60px;
} }
.sidenav { .sidenav {
+6 -1
View File
@@ -4271,7 +4271,12 @@ $.when($.ready).then(function () {
data.url = apiurl; data.url = apiurl;
$(".config-item").each(function () { $(".config-item").each(function () {
var config = $(this).data("config"); var config = $(this).data("config");
data[config] = $(this).val(); // For checkboxes, use checked state instead of value attribute
if ($(this).is(":checkbox")) {
data[config] = $(this).is(":checked") ? "1" : "0";
} else {
data[config] = $(this).val();
}
}); });
data.id = $("form[data-item-id]").data("item-id"); data.id = $("form[data-item-id]").data("item-id");
if (data.password && data.password === fakePassword) { if (data.password && data.password === fakePassword) {
+6 -1
View File
@@ -310,7 +310,12 @@ $.when($.ready).then(() => {
data.url = apiurl; data.url = apiurl;
$(".config-item").each(function () { $(".config-item").each(function () {
const config = $(this).data("config"); const config = $(this).data("config");
data[config] = $(this).val(); // For checkboxes, use checked state instead of value attribute
if ($(this).is(":checkbox")) {
data[config] = $(this).is(":checked") ? "1" : "0";
} else {
data[config] = $(this).val();
}
}); });
data.id = $("form[data-item-id]").data("item-id"); data.id = $("form[data-item-id]").data("item-id");
+3 -2
View File
@@ -722,9 +722,10 @@ div.create {
flex: 0 0 60px; flex: 0 0 60px;
} }
.app-icon { .app-icon {
max-width: 60px; width: 60px;
height: 60px;
object-fit: contain;
display: block; display: block;
max-height: 60px;
} }
.sidenav { .sidenav {
+127
View File
@@ -33,6 +33,7 @@ return array(
'App\\Console\\Commands\\RegisterApp' => $baseDir . '/app/Console/Commands/RegisterApp.php', 'App\\Console\\Commands\\RegisterApp' => $baseDir . '/app/Console/Commands/RegisterApp.php',
'App\\EnhancedApps' => $baseDir . '/app/EnhancedApps.php', 'App\\EnhancedApps' => $baseDir . '/app/EnhancedApps.php',
'App\\Facades\\Form' => $baseDir . '/app/Facades/Form.php', 'App\\Facades\\Form' => $baseDir . '/app/Facades/Form.php',
'App\\Helpers\\TrueNASWebSocketClient' => $baseDir . '/app/Helpers/TrueNASWebSocketClient.php',
'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => $baseDir . '/app/Http/Controllers/Auth/ForgotPasswordController.php', 'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => $baseDir . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
'App\\Http\\Controllers\\Auth\\LoginController' => $baseDir . '/app/Http/Controllers/Auth/LoginController.php', 'App\\Http\\Controllers\\Auth\\LoginController' => $baseDir . '/app/Http/Controllers/Auth/LoginController.php',
'App\\Http\\Controllers\\Auth\\RegisterController' => $baseDir . '/app/Http/Controllers/Auth/RegisterController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => $baseDir . '/app/Http/Controllers/Auth/RegisterController.php',
@@ -5199,6 +5200,17 @@ return array(
'NunoMaduro\\Collision\\Provider' => $vendorDir . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\Provider' => $vendorDir . '/nunomaduro/collision/src/Provider.php',
'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => $vendorDir . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => $vendorDir . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php',
'NunoMaduro\\Collision\\Writer' => $vendorDir . '/nunomaduro/collision/src/Writer.php', 'NunoMaduro\\Collision\\Writer' => $vendorDir . '/nunomaduro/collision/src/Writer.php',
'Nyholm\\Psr7\\Factory\\HttplugFactory' => $vendorDir . '/nyholm/psr7/src/Factory/HttplugFactory.php',
'Nyholm\\Psr7\\Factory\\Psr17Factory' => $vendorDir . '/nyholm/psr7/src/Factory/Psr17Factory.php',
'Nyholm\\Psr7\\MessageTrait' => $vendorDir . '/nyholm/psr7/src/MessageTrait.php',
'Nyholm\\Psr7\\Request' => $vendorDir . '/nyholm/psr7/src/Request.php',
'Nyholm\\Psr7\\RequestTrait' => $vendorDir . '/nyholm/psr7/src/RequestTrait.php',
'Nyholm\\Psr7\\Response' => $vendorDir . '/nyholm/psr7/src/Response.php',
'Nyholm\\Psr7\\ServerRequest' => $vendorDir . '/nyholm/psr7/src/ServerRequest.php',
'Nyholm\\Psr7\\Stream' => $vendorDir . '/nyholm/psr7/src/Stream.php',
'Nyholm\\Psr7\\StreamTrait' => $vendorDir . '/nyholm/psr7/src/StreamTrait.php',
'Nyholm\\Psr7\\UploadedFile' => $vendorDir . '/nyholm/psr7/src/UploadedFile.php',
'Nyholm\\Psr7\\Uri' => $vendorDir . '/nyholm/psr7/src/Uri.php',
'PHPUnit\\Event\\Application\\Finished' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Finished.php', 'PHPUnit\\Event\\Application\\Finished' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Finished.php',
'PHPUnit\\Event\\Application\\FinishedSubscriber' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php', 'PHPUnit\\Event\\Application\\FinishedSubscriber' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php',
'PHPUnit\\Event\\Application\\Started' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Started.php', 'PHPUnit\\Event\\Application\\Started' => $vendorDir . '/phpunit/phpunit/src/Event/Events/Application/Started.php',
@@ -6553,6 +6565,51 @@ return array(
'PhpParser\\PrettyPrinter\\Standard' => $vendorDir . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php', 'PhpParser\\PrettyPrinter\\Standard' => $vendorDir . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php',
'PhpParser\\Token' => $vendorDir . '/nikic/php-parser/lib/PhpParser/Token.php', 'PhpParser\\Token' => $vendorDir . '/nikic/php-parser/lib/PhpParser/Token.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Phrity\\Comparison\\Comparable' => $vendorDir . '/phrity/comparison/src/Comparable.php',
'Phrity\\Comparison\\Comparator' => $vendorDir . '/phrity/comparison/src/Comparator.php',
'Phrity\\Comparison\\ComparisonTrait' => $vendorDir . '/phrity/comparison/src/ComparisonTrait.php',
'Phrity\\Comparison\\Equalable' => $vendorDir . '/phrity/comparison/src/Equalable.php',
'Phrity\\Comparison\\IncomparableException' => $vendorDir . '/phrity/comparison/src/IncomparableException.php',
'Phrity\\Http\\HttpFactory' => $vendorDir . '/phrity/http/src/HttpFactory.php',
'Phrity\\Http\\Serializer' => $vendorDir . '/phrity/http/src/Serializer.php',
'Phrity\\Net\\Context' => $vendorDir . '/phrity/net-stream/src/Context.php',
'Phrity\\Net\\SocketClient' => $vendorDir . '/phrity/net-stream/src/SocketClient.php',
'Phrity\\Net\\SocketServer' => $vendorDir . '/phrity/net-stream/src/SocketServer.php',
'Phrity\\Net\\SocketStream' => $vendorDir . '/phrity/net-stream/src/SocketStream.php',
'Phrity\\Net\\Stream' => $vendorDir . '/phrity/net-stream/src/Stream.php',
'Phrity\\Net\\StreamCollection' => $vendorDir . '/phrity/net-stream/src/StreamCollection.php',
'Phrity\\Net\\StreamContainerInterface' => $vendorDir . '/phrity/net-stream/src/StreamContainerInterface.php',
'Phrity\\Net\\StreamException' => $vendorDir . '/phrity/net-stream/src/StreamException.php',
'Phrity\\Net\\StreamFactory' => $vendorDir . '/phrity/net-stream/src/StreamFactory.php',
'Phrity\\Net\\StreamInterface' => $vendorDir . '/phrity/net-stream/src/StreamInterface.php',
'Phrity\\Net\\Uri' => $vendorDir . '/phrity/net-uri/src/Uri.php',
'Phrity\\Net\\UriFactory' => $vendorDir . '/phrity/net-uri/src/UriFactory.php',
'Phrity\\Util\\Accessor' => $vendorDir . '/phrity/util-accessor/src/Accessor.php',
'Phrity\\Util\\AccessorException' => $vendorDir . '/phrity/util-accessor/src/AccessorException.php',
'Phrity\\Util\\AccessorTrait' => $vendorDir . '/phrity/util-accessor/src/AccessorTrait.php',
'Phrity\\Util\\DataAccessor' => $vendorDir . '/phrity/util-accessor/src/DataAccessor.php',
'Phrity\\Util\\ErrorHandler' => $vendorDir . '/phrity/util-errorhandler/src/ErrorHandler.php',
'Phrity\\Util\\Interpolator\\Interpolator' => $vendorDir . '/phrity/util-interpolator/src/Interpolator.php',
'Phrity\\Util\\Interpolator\\InterpolatorTrait' => $vendorDir . '/phrity/util-interpolator/src/InterpolatorTrait.php',
'Phrity\\Util\\PathAccessor' => $vendorDir . '/phrity/util-accessor/src/PathAccessor.php',
'Phrity\\Util\\Transformer\\BasicTypeConverter' => $vendorDir . '/phrity/util-transformer/src/BasicTypeConverter.php',
'Phrity\\Util\\Transformer\\ChainedResolver' => $vendorDir . '/phrity/util-transformer/src/ChainedResolver.php',
'Phrity\\Util\\Transformer\\DateTimeConverter' => $vendorDir . '/phrity/util-transformer/src/DateTimeConverter.php',
'Phrity\\Util\\Transformer\\EnumConverter' => $vendorDir . '/phrity/util-transformer/src/EnumConverter.php',
'Phrity\\Util\\Transformer\\FirstMatchResolver' => $vendorDir . '/phrity/util-transformer/src/FirstMatchResolver.php',
'Phrity\\Util\\Transformer\\FlattenDecoder' => $vendorDir . '/phrity/util-transformer/src/FlattenDecoder.php',
'Phrity\\Util\\Transformer\\JsonDecoder' => $vendorDir . '/phrity/util-transformer/src/JsonDecoder.php',
'Phrity\\Util\\Transformer\\JsonSerializableConverter' => $vendorDir . '/phrity/util-transformer/src/JsonSerializableConverter.php',
'Phrity\\Util\\Transformer\\ReadableConverter' => $vendorDir . '/phrity/util-transformer/src/ReadableConverter.php',
'Phrity\\Util\\Transformer\\RecursionResolver' => $vendorDir . '/phrity/util-transformer/src/RecursionResolver.php',
'Phrity\\Util\\Transformer\\ReversedReadableConverter' => $vendorDir . '/phrity/util-transformer/src/ReversedReadableConverter.php',
'Phrity\\Util\\Transformer\\StringResolver' => $vendorDir . '/phrity/util-transformer/src/StringResolver.php',
'Phrity\\Util\\Transformer\\StringableConverter' => $vendorDir . '/phrity/util-transformer/src/StringableConverter.php',
'Phrity\\Util\\Transformer\\Symfony\\NormalizerWrapper' => $vendorDir . '/phrity/util-transformer/src/Symfony/NormalizerWrapper.php',
'Phrity\\Util\\Transformer\\ThrowableConverter' => $vendorDir . '/phrity/util-transformer/src/ThrowableConverter.php',
'Phrity\\Util\\Transformer\\TransformerException' => $vendorDir . '/phrity/util-transformer/src/TransformerException.php',
'Phrity\\Util\\Transformer\\TransformerInterface' => $vendorDir . '/phrity/util-transformer/src/TransformerInterface.php',
'Phrity\\Util\\Transformer\\Type' => $vendorDir . '/phrity/util-transformer/src/Type.php',
'Psr\\Cache\\CacheException' => $vendorDir . '/psr/cache/src/CacheException.php', 'Psr\\Cache\\CacheException' => $vendorDir . '/psr/cache/src/CacheException.php',
'Psr\\Cache\\CacheItemInterface' => $vendorDir . '/psr/cache/src/CacheItemInterface.php', 'Psr\\Cache\\CacheItemInterface' => $vendorDir . '/psr/cache/src/CacheItemInterface.php',
'Psr\\Cache\\CacheItemPoolInterface' => $vendorDir . '/psr/cache/src/CacheItemPoolInterface.php', 'Psr\\Cache\\CacheItemPoolInterface' => $vendorDir . '/psr/cache/src/CacheItemPoolInterface.php',
@@ -8851,6 +8908,8 @@ return array(
'Termwind\\ValueObjects\\Node' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Node.php', 'Termwind\\ValueObjects\\Node' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Node.php',
'Termwind\\ValueObjects\\Style' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Style' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Style.php',
'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Termwind\\ValueObjects\\Styles' => $vendorDir . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
'Tests\\Feature\\AjaxPostEndpointsTest' => $baseDir . '/tests/Feature/AjaxPostEndpointsTest.php',
'Tests\\Feature\\CsrfExceptionsTest' => $baseDir . '/tests/Feature/CsrfExceptionsTest.php',
'Tests\\Feature\\DashTest' => $baseDir . '/tests/Feature/DashTest.php', 'Tests\\Feature\\DashTest' => $baseDir . '/tests/Feature/DashTest.php',
'Tests\\Feature\\GetStatsTest' => $baseDir . '/tests/Feature/GetStatsTest.php', 'Tests\\Feature\\GetStatsTest' => $baseDir . '/tests/Feature/GetStatsTest.php',
'Tests\\Feature\\ImportTest' => $baseDir . '/tests/Feature/ImportTest.php', 'Tests\\Feature\\ImportTest' => $baseDir . '/tests/Feature/ImportTest.php',
@@ -8859,16 +8918,24 @@ return array(
'Tests\\Feature\\ItemExportTest' => $baseDir . '/tests/Feature/ItemExportTest.php', 'Tests\\Feature\\ItemExportTest' => $baseDir . '/tests/Feature/ItemExportTest.php',
'Tests\\Feature\\ItemImportTest' => $baseDir . '/tests/Feature/ItemImportTest.php', 'Tests\\Feature\\ItemImportTest' => $baseDir . '/tests/Feature/ItemImportTest.php',
'Tests\\Feature\\ItemListTest' => $baseDir . '/tests/Feature/ItemListTest.php', 'Tests\\Feature\\ItemListTest' => $baseDir . '/tests/Feature/ItemListTest.php',
'Tests\\Feature\\ItemOwnershipTest' => $baseDir . '/tests/Feature/ItemOwnershipTest.php',
'Tests\\Feature\\OrphanItemRecoveryTest' => $baseDir . '/tests/Feature/OrphanItemRecoveryTest.php',
'Tests\\Feature\\RoutesRenderTest' => $baseDir . '/tests/Feature/RoutesRenderTest.php',
'Tests\\Feature\\SearchTest' => $baseDir . '/tests/Feature/SearchTest.php', 'Tests\\Feature\\SearchTest' => $baseDir . '/tests/Feature/SearchTest.php',
'Tests\\Feature\\SettingsTest' => $baseDir . '/tests/Feature/SettingsTest.php', 'Tests\\Feature\\SettingsTest' => $baseDir . '/tests/Feature/SettingsTest.php',
'Tests\\Feature\\StorageDiskTest' => $baseDir . '/tests/Feature/StorageDiskTest.php',
'Tests\\Feature\\TagListTest' => $baseDir . '/tests/Feature/TagListTest.php', 'Tests\\Feature\\TagListTest' => $baseDir . '/tests/Feature/TagListTest.php',
'Tests\\Feature\\TrustHostsTest' => $baseDir . '/tests/Feature/TrustHostsTest.php', 'Tests\\Feature\\TrustHostsTest' => $baseDir . '/tests/Feature/TrustHostsTest.php',
'Tests\\Feature\\TrustProxiesTest' => $baseDir . '/tests/Feature/TrustProxiesTest.php', 'Tests\\Feature\\TrustProxiesTest' => $baseDir . '/tests/Feature/TrustProxiesTest.php',
'Tests\\Feature\\UserDeleteItemsTest' => $baseDir . '/tests/Feature/UserDeleteItemsTest.php',
'Tests\\Feature\\UserEditTest' => $baseDir . '/tests/Feature/UserEditTest.php', 'Tests\\Feature\\UserEditTest' => $baseDir . '/tests/Feature/UserEditTest.php',
'Tests\\Feature\\UserListTest' => $baseDir . '/tests/Feature/UserListTest.php', 'Tests\\Feature\\UserListTest' => $baseDir . '/tests/Feature/UserListTest.php',
'Tests\\TestCase' => $baseDir . '/tests/TestCase.php', 'Tests\\TestCase' => $baseDir . '/tests/TestCase.php',
'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => $baseDir . '/tests/Unit/database/seeders/SettingsSeederTest.php', 'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => $baseDir . '/tests/Unit/database/seeders/SettingsSeederTest.php',
'Tests\\Unit\\helpers\\ClassNameTest' => $baseDir . '/tests/Unit/helpers/ClassNameTest.php',
'Tests\\Unit\\helpers\\ColorHelpersTest' => $baseDir . '/tests/Unit/helpers/ColorHelpersTest.php',
'Tests\\Unit\\helpers\\IsImageTest' => $baseDir . '/tests/Unit/helpers/IsImageTest.php', 'Tests\\Unit\\helpers\\IsImageTest' => $baseDir . '/tests/Unit/helpers/IsImageTest.php',
'Tests\\Unit\\helpers\\SizeHelpersTest' => $baseDir . '/tests/Unit/helpers/SizeHelpersTest.php',
'Tests\\Unit\\helpers\\SlugTest' => $baseDir . '/tests/Unit/helpers/SlugTest.php', 'Tests\\Unit\\helpers\\SlugTest' => $baseDir . '/tests/Unit/helpers/SlugTest.php',
'Tests\\Unit\\lang\\LangTest' => $baseDir . '/tests/Unit/lang/LangTest.php', 'Tests\\Unit\\lang\\LangTest' => $baseDir . '/tests/Unit/lang/LangTest.php',
'TheSeer\\Tokenizer\\Exception' => $vendorDir . '/theseer/tokenizer/src/Exception.php', 'TheSeer\\Tokenizer\\Exception' => $vendorDir . '/theseer/tokenizer/src/Exception.php',
@@ -8887,6 +8954,66 @@ return array(
'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => $vendorDir . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php', 'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => $vendorDir . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
'WebSocket\\Client' => $vendorDir . '/phrity/websocket/src/Client.php',
'WebSocket\\Configuration' => $vendorDir . '/phrity/websocket/src/Configuration.php',
'WebSocket\\Connection' => $vendorDir . '/phrity/websocket/src/Connection.php',
'WebSocket\\Constant' => $vendorDir . '/phrity/websocket/src/Constant.php',
'WebSocket\\Exception\\BadOpcodeException' => $vendorDir . '/phrity/websocket/src/Exception/BadOpcodeException.php',
'WebSocket\\Exception\\BadUriException' => $vendorDir . '/phrity/websocket/src/Exception/BadUriException.php',
'WebSocket\\Exception\\ClientException' => $vendorDir . '/phrity/websocket/src/Exception/ClientException.php',
'WebSocket\\Exception\\CloseException' => $vendorDir . '/phrity/websocket/src/Exception/CloseException.php',
'WebSocket\\Exception\\ConnectionClosedException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionClosedException.php',
'WebSocket\\Exception\\ConnectionFailureException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionFailureException.php',
'WebSocket\\Exception\\ConnectionLevelInterface' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionLevelInterface.php',
'WebSocket\\Exception\\ConnectionTimeoutException' => $vendorDir . '/phrity/websocket/src/Exception/ConnectionTimeoutException.php',
'WebSocket\\Exception\\Exception' => $vendorDir . '/phrity/websocket/src/Exception/Exception.php',
'WebSocket\\Exception\\ExceptionInterface' => $vendorDir . '/phrity/websocket/src/Exception/ExceptionInterface.php',
'WebSocket\\Exception\\HandshakeException' => $vendorDir . '/phrity/websocket/src/Exception/HandshakeException.php',
'WebSocket\\Exception\\MessageLevelInterface' => $vendorDir . '/phrity/websocket/src/Exception/MessageLevelInterface.php',
'WebSocket\\Exception\\ReconnectException' => $vendorDir . '/phrity/websocket/src/Exception/ReconnectException.php',
'WebSocket\\Exception\\RunnerException' => $vendorDir . '/phrity/websocket/src/Exception/RunnerException.php',
'WebSocket\\Exception\\ServerException' => $vendorDir . '/phrity/websocket/src/Exception/ServerException.php',
'WebSocket\\Frame\\Frame' => $vendorDir . '/phrity/websocket/src/Frame/Frame.php',
'WebSocket\\Frame\\FrameHandler' => $vendorDir . '/phrity/websocket/src/Frame/FrameHandler.php',
'WebSocket\\Http\\DefaultHttpFactory' => $vendorDir . '/phrity/websocket/src/Http/DefaultHttpFactory.php',
'WebSocket\\Http\\HttpHandler' => $vendorDir . '/phrity/websocket/src/Http/HttpHandler.php',
'WebSocket\\Http\\Request' => $vendorDir . '/phrity/websocket/src/Http/Request.php',
'WebSocket\\Http\\Response' => $vendorDir . '/phrity/websocket/src/Http/Response.php',
'WebSocket\\Http\\ServerRequest' => $vendorDir . '/phrity/websocket/src/Http/ServerRequest.php',
'WebSocket\\Message\\Binary' => $vendorDir . '/phrity/websocket/src/Message/Binary.php',
'WebSocket\\Message\\Close' => $vendorDir . '/phrity/websocket/src/Message/Close.php',
'WebSocket\\Message\\Message' => $vendorDir . '/phrity/websocket/src/Message/Message.php',
'WebSocket\\Message\\MessageHandler' => $vendorDir . '/phrity/websocket/src/Message/MessageHandler.php',
'WebSocket\\Message\\Ping' => $vendorDir . '/phrity/websocket/src/Message/Ping.php',
'WebSocket\\Message\\Pong' => $vendorDir . '/phrity/websocket/src/Message/Pong.php',
'WebSocket\\Message\\Text' => $vendorDir . '/phrity/websocket/src/Message/Text.php',
'WebSocket\\Middleware\\Callback' => $vendorDir . '/phrity/websocket/src/Middleware/Callback.php',
'WebSocket\\Middleware\\CloseHandler' => $vendorDir . '/phrity/websocket/src/Middleware/CloseHandler.php',
'WebSocket\\Middleware\\CompressionExtension' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension.php',
'WebSocket\\Middleware\\CompressionExtension\\CompressorInterface' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php',
'WebSocket\\Middleware\\CompressionExtension\\DeflateCompressor' => $vendorDir . '/phrity/websocket/src/Middleware/CompressionExtension/DeflateCompressor.php',
'WebSocket\\Middleware\\FollowRedirect' => $vendorDir . '/phrity/websocket/src/Middleware/FollowRedirect.php',
'WebSocket\\Middleware\\MiddlewareHandler' => $vendorDir . '/phrity/websocket/src/Middleware/MiddlewareHandler.php',
'WebSocket\\Middleware\\MiddlewareInterface' => $vendorDir . '/phrity/websocket/src/Middleware/MiddlewareInterface.php',
'WebSocket\\Middleware\\PingInterval' => $vendorDir . '/phrity/websocket/src/Middleware/PingInterval.php',
'WebSocket\\Middleware\\PingResponder' => $vendorDir . '/phrity/websocket/src/Middleware/PingResponder.php',
'WebSocket\\Middleware\\ProcessHttpIncomingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php',
'WebSocket\\Middleware\\ProcessHttpOutgoingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpOutgoingInterface.php',
'WebSocket\\Middleware\\ProcessHttpStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessHttpStack.php',
'WebSocket\\Middleware\\ProcessIncomingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessIncomingInterface.php',
'WebSocket\\Middleware\\ProcessOutgoingInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessOutgoingInterface.php',
'WebSocket\\Middleware\\ProcessStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessStack.php',
'WebSocket\\Middleware\\ProcessTickInterface' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessTickInterface.php',
'WebSocket\\Middleware\\ProcessTickStack' => $vendorDir . '/phrity/websocket/src/Middleware/ProcessTickStack.php',
'WebSocket\\Middleware\\SubprotocolNegotiation' => $vendorDir . '/phrity/websocket/src/Middleware/SubprotocolNegotiation.php',
'WebSocket\\Runtime\\IdentityInterface' => $vendorDir . '/phrity/websocket/src/Runtime/IdentityInterface.php',
'WebSocket\\Runtime\\Runner' => $vendorDir . '/phrity/websocket/src/Runtime/Runner.php',
'WebSocket\\Server' => $vendorDir . '/phrity/websocket/src/Server.php',
'WebSocket\\Trait\\ConfigurationTrait' => $vendorDir . '/phrity/websocket/src/Trait/ConfigurationTrait.php',
'WebSocket\\Trait\\ListenerTrait' => $vendorDir . '/phrity/websocket/src/Trait/ListenerTrait.php',
'WebSocket\\Trait\\OpcodeTrait' => $vendorDir . '/phrity/websocket/src/Trait/OpcodeTrait.php',
'WebSocket\\Trait\\SendMethodsTrait' => $vendorDir . '/phrity/websocket/src/Trait/SendMethodsTrait.php',
'WebSocket\\Trait\\StringableTrait' => $vendorDir . '/phrity/websocket/src/Trait/StringableTrait.php',
'Whoops\\Exception\\ErrorException' => $vendorDir . '/filp/whoops/src/Whoops/Exception/ErrorException.php', 'Whoops\\Exception\\ErrorException' => $vendorDir . '/filp/whoops/src/Whoops/Exception/ErrorException.php',
'Whoops\\Exception\\Formatter' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Formatter.php', 'Whoops\\Exception\\Formatter' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Formatter.php',
'Whoops\\Exception\\Frame' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Frame.php', 'Whoops\\Exception\\Frame' => $vendorDir . '/filp/whoops/src/Whoops/Exception/Frame.php',
+8
View File
@@ -9,6 +9,7 @@ return array(
'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'), 'voku\\' => array($vendorDir . '/voku/portable-ascii/src/voku'),
'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'), 'enshrined\\svgSanitize\\' => array($vendorDir . '/enshrined/svg-sanitize/src'),
'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'), 'Whoops\\' => array($vendorDir . '/filp/whoops/src/Whoops'),
'WebSocket\\' => array($vendorDir . '/phrity/websocket/src'),
'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'), 'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
'Tests\\' => array($baseDir . '/tests'), 'Tests\\' => array($baseDir . '/tests'),
'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'), 'Termwind\\' => array($vendorDir . '/nunomaduro/termwind/src'),
@@ -66,8 +67,15 @@ return array(
'Psr\\Container\\' => array($vendorDir . '/psr/container/src'), 'Psr\\Container\\' => array($vendorDir . '/psr/container/src'),
'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'), 'Psr\\Clock\\' => array($vendorDir . '/psr/clock/src'),
'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'), 'Psr\\Cache\\' => array($vendorDir . '/psr/cache/src'),
'Phrity\\Util\\Transformer\\' => array($vendorDir . '/phrity/util-transformer/src'),
'Phrity\\Util\\Interpolator\\' => array($vendorDir . '/phrity/util-interpolator/src'),
'Phrity\\Util\\' => array($vendorDir . '/phrity/util-errorhandler/src', $vendorDir . '/phrity/util-accessor/src'),
'Phrity\\Net\\' => array($vendorDir . '/phrity/net-uri/src', $vendorDir . '/phrity/net-stream/src'),
'Phrity\\Http\\' => array($vendorDir . '/phrity/http/src'),
'Phrity\\Comparison\\' => array($vendorDir . '/phrity/comparison/src'),
'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'), 'PhpParser\\' => array($vendorDir . '/nikic/php-parser/lib/PhpParser'),
'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'), 'PhpOption\\' => array($vendorDir . '/phpoption/phpoption/src/PhpOption'),
'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'), 'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'), 'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'), 'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
+169
View File
@@ -65,6 +65,7 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'W' => 'W' =>
array ( array (
'Whoops\\' => 7, 'Whoops\\' => 7,
'WebSocket\\' => 10,
), ),
'T' => 'T' =>
array ( array (
@@ -134,11 +135,18 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'Psr\\Container\\' => 14, 'Psr\\Container\\' => 14,
'Psr\\Clock\\' => 10, 'Psr\\Clock\\' => 10,
'Psr\\Cache\\' => 10, 'Psr\\Cache\\' => 10,
'Phrity\\Util\\Transformer\\' => 24,
'Phrity\\Util\\Interpolator\\' => 25,
'Phrity\\Util\\' => 12,
'Phrity\\Net\\' => 11,
'Phrity\\Http\\' => 12,
'Phrity\\Comparison\\' => 18,
'PhpParser\\' => 10, 'PhpParser\\' => 10,
'PhpOption\\' => 10, 'PhpOption\\' => 10,
), ),
'N' => 'N' =>
array ( array (
'Nyholm\\Psr7\\' => 12,
'NunoMaduro\\Collision\\' => 21, 'NunoMaduro\\Collision\\' => 21,
'Nette\\' => 6, 'Nette\\' => 6,
), ),
@@ -247,6 +255,10 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
array ( array (
0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops', 0 => __DIR__ . '/..' . '/filp/whoops/src/Whoops',
), ),
'WebSocket\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/websocket/src',
),
'TijsVerkoyen\\CssToInlineStyles\\' => 'TijsVerkoyen\\CssToInlineStyles\\' =>
array ( array (
0 => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src', 0 => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src',
@@ -478,6 +490,32 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
array ( array (
0 => __DIR__ . '/..' . '/psr/cache/src', 0 => __DIR__ . '/..' . '/psr/cache/src',
), ),
'Phrity\\Util\\Transformer\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/util-transformer/src',
),
'Phrity\\Util\\Interpolator\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/util-interpolator/src',
),
'Phrity\\Util\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/util-errorhandler/src',
1 => __DIR__ . '/..' . '/phrity/util-accessor/src',
),
'Phrity\\Net\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/net-uri/src',
1 => __DIR__ . '/..' . '/phrity/net-stream/src',
),
'Phrity\\Http\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/http/src',
),
'Phrity\\Comparison\\' =>
array (
0 => __DIR__ . '/..' . '/phrity/comparison/src',
),
'PhpParser\\' => 'PhpParser\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser', 0 => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser',
@@ -486,6 +524,10 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
array ( array (
0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption', 0 => __DIR__ . '/..' . '/phpoption/phpoption/src/PhpOption',
), ),
'Nyholm\\Psr7\\' =>
array (
0 => __DIR__ . '/..' . '/nyholm/psr7/src',
),
'NunoMaduro\\Collision\\' => 'NunoMaduro\\Collision\\' =>
array ( array (
0 => __DIR__ . '/..' . '/nunomaduro/collision/src', 0 => __DIR__ . '/..' . '/nunomaduro/collision/src',
@@ -755,6 +797,7 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'App\\Console\\Commands\\RegisterApp' => __DIR__ . '/../..' . '/app/Console/Commands/RegisterApp.php', 'App\\Console\\Commands\\RegisterApp' => __DIR__ . '/../..' . '/app/Console/Commands/RegisterApp.php',
'App\\EnhancedApps' => __DIR__ . '/../..' . '/app/EnhancedApps.php', 'App\\EnhancedApps' => __DIR__ . '/../..' . '/app/EnhancedApps.php',
'App\\Facades\\Form' => __DIR__ . '/../..' . '/app/Facades/Form.php', 'App\\Facades\\Form' => __DIR__ . '/../..' . '/app/Facades/Form.php',
'App\\Helpers\\TrueNASWebSocketClient' => __DIR__ . '/../..' . '/app/Helpers/TrueNASWebSocketClient.php',
'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php', 'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php', 'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',
'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
@@ -5921,6 +5964,17 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'NunoMaduro\\Collision\\Provider' => __DIR__ . '/..' . '/nunomaduro/collision/src/Provider.php', 'NunoMaduro\\Collision\\Provider' => __DIR__ . '/..' . '/nunomaduro/collision/src/Provider.php',
'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => __DIR__ . '/..' . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php', 'NunoMaduro\\Collision\\SolutionsRepositories\\NullSolutionsRepository' => __DIR__ . '/..' . '/nunomaduro/collision/src/SolutionsRepositories/NullSolutionsRepository.php',
'NunoMaduro\\Collision\\Writer' => __DIR__ . '/..' . '/nunomaduro/collision/src/Writer.php', 'NunoMaduro\\Collision\\Writer' => __DIR__ . '/..' . '/nunomaduro/collision/src/Writer.php',
'Nyholm\\Psr7\\Factory\\HttplugFactory' => __DIR__ . '/..' . '/nyholm/psr7/src/Factory/HttplugFactory.php',
'Nyholm\\Psr7\\Factory\\Psr17Factory' => __DIR__ . '/..' . '/nyholm/psr7/src/Factory/Psr17Factory.php',
'Nyholm\\Psr7\\MessageTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/MessageTrait.php',
'Nyholm\\Psr7\\Request' => __DIR__ . '/..' . '/nyholm/psr7/src/Request.php',
'Nyholm\\Psr7\\RequestTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/RequestTrait.php',
'Nyholm\\Psr7\\Response' => __DIR__ . '/..' . '/nyholm/psr7/src/Response.php',
'Nyholm\\Psr7\\ServerRequest' => __DIR__ . '/..' . '/nyholm/psr7/src/ServerRequest.php',
'Nyholm\\Psr7\\Stream' => __DIR__ . '/..' . '/nyholm/psr7/src/Stream.php',
'Nyholm\\Psr7\\StreamTrait' => __DIR__ . '/..' . '/nyholm/psr7/src/StreamTrait.php',
'Nyholm\\Psr7\\UploadedFile' => __DIR__ . '/..' . '/nyholm/psr7/src/UploadedFile.php',
'Nyholm\\Psr7\\Uri' => __DIR__ . '/..' . '/nyholm/psr7/src/Uri.php',
'PHPUnit\\Event\\Application\\Finished' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Finished.php', 'PHPUnit\\Event\\Application\\Finished' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Finished.php',
'PHPUnit\\Event\\Application\\FinishedSubscriber' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php', 'PHPUnit\\Event\\Application\\FinishedSubscriber' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/FinishedSubscriber.php',
'PHPUnit\\Event\\Application\\Started' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Started.php', 'PHPUnit\\Event\\Application\\Started' => __DIR__ . '/..' . '/phpunit/phpunit/src/Event/Events/Application/Started.php',
@@ -7275,6 +7329,51 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'PhpParser\\PrettyPrinter\\Standard' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php', 'PhpParser\\PrettyPrinter\\Standard' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/PrettyPrinter/Standard.php',
'PhpParser\\Token' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/Token.php', 'PhpParser\\Token' => __DIR__ . '/..' . '/nikic/php-parser/lib/PhpParser/Token.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Phrity\\Comparison\\Comparable' => __DIR__ . '/..' . '/phrity/comparison/src/Comparable.php',
'Phrity\\Comparison\\Comparator' => __DIR__ . '/..' . '/phrity/comparison/src/Comparator.php',
'Phrity\\Comparison\\ComparisonTrait' => __DIR__ . '/..' . '/phrity/comparison/src/ComparisonTrait.php',
'Phrity\\Comparison\\Equalable' => __DIR__ . '/..' . '/phrity/comparison/src/Equalable.php',
'Phrity\\Comparison\\IncomparableException' => __DIR__ . '/..' . '/phrity/comparison/src/IncomparableException.php',
'Phrity\\Http\\HttpFactory' => __DIR__ . '/..' . '/phrity/http/src/HttpFactory.php',
'Phrity\\Http\\Serializer' => __DIR__ . '/..' . '/phrity/http/src/Serializer.php',
'Phrity\\Net\\Context' => __DIR__ . '/..' . '/phrity/net-stream/src/Context.php',
'Phrity\\Net\\SocketClient' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketClient.php',
'Phrity\\Net\\SocketServer' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketServer.php',
'Phrity\\Net\\SocketStream' => __DIR__ . '/..' . '/phrity/net-stream/src/SocketStream.php',
'Phrity\\Net\\Stream' => __DIR__ . '/..' . '/phrity/net-stream/src/Stream.php',
'Phrity\\Net\\StreamCollection' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamCollection.php',
'Phrity\\Net\\StreamContainerInterface' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamContainerInterface.php',
'Phrity\\Net\\StreamException' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamException.php',
'Phrity\\Net\\StreamFactory' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamFactory.php',
'Phrity\\Net\\StreamInterface' => __DIR__ . '/..' . '/phrity/net-stream/src/StreamInterface.php',
'Phrity\\Net\\Uri' => __DIR__ . '/..' . '/phrity/net-uri/src/Uri.php',
'Phrity\\Net\\UriFactory' => __DIR__ . '/..' . '/phrity/net-uri/src/UriFactory.php',
'Phrity\\Util\\Accessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/Accessor.php',
'Phrity\\Util\\AccessorException' => __DIR__ . '/..' . '/phrity/util-accessor/src/AccessorException.php',
'Phrity\\Util\\AccessorTrait' => __DIR__ . '/..' . '/phrity/util-accessor/src/AccessorTrait.php',
'Phrity\\Util\\DataAccessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/DataAccessor.php',
'Phrity\\Util\\ErrorHandler' => __DIR__ . '/..' . '/phrity/util-errorhandler/src/ErrorHandler.php',
'Phrity\\Util\\Interpolator\\Interpolator' => __DIR__ . '/..' . '/phrity/util-interpolator/src/Interpolator.php',
'Phrity\\Util\\Interpolator\\InterpolatorTrait' => __DIR__ . '/..' . '/phrity/util-interpolator/src/InterpolatorTrait.php',
'Phrity\\Util\\PathAccessor' => __DIR__ . '/..' . '/phrity/util-accessor/src/PathAccessor.php',
'Phrity\\Util\\Transformer\\BasicTypeConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/BasicTypeConverter.php',
'Phrity\\Util\\Transformer\\ChainedResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/ChainedResolver.php',
'Phrity\\Util\\Transformer\\DateTimeConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/DateTimeConverter.php',
'Phrity\\Util\\Transformer\\EnumConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/EnumConverter.php',
'Phrity\\Util\\Transformer\\FirstMatchResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/FirstMatchResolver.php',
'Phrity\\Util\\Transformer\\FlattenDecoder' => __DIR__ . '/..' . '/phrity/util-transformer/src/FlattenDecoder.php',
'Phrity\\Util\\Transformer\\JsonDecoder' => __DIR__ . '/..' . '/phrity/util-transformer/src/JsonDecoder.php',
'Phrity\\Util\\Transformer\\JsonSerializableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/JsonSerializableConverter.php',
'Phrity\\Util\\Transformer\\ReadableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ReadableConverter.php',
'Phrity\\Util\\Transformer\\RecursionResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/RecursionResolver.php',
'Phrity\\Util\\Transformer\\ReversedReadableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ReversedReadableConverter.php',
'Phrity\\Util\\Transformer\\StringResolver' => __DIR__ . '/..' . '/phrity/util-transformer/src/StringResolver.php',
'Phrity\\Util\\Transformer\\StringableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/StringableConverter.php',
'Phrity\\Util\\Transformer\\Symfony\\NormalizerWrapper' => __DIR__ . '/..' . '/phrity/util-transformer/src/Symfony/NormalizerWrapper.php',
'Phrity\\Util\\Transformer\\ThrowableConverter' => __DIR__ . '/..' . '/phrity/util-transformer/src/ThrowableConverter.php',
'Phrity\\Util\\Transformer\\TransformerException' => __DIR__ . '/..' . '/phrity/util-transformer/src/TransformerException.php',
'Phrity\\Util\\Transformer\\TransformerInterface' => __DIR__ . '/..' . '/phrity/util-transformer/src/TransformerInterface.php',
'Phrity\\Util\\Transformer\\Type' => __DIR__ . '/..' . '/phrity/util-transformer/src/Type.php',
'Psr\\Cache\\CacheException' => __DIR__ . '/..' . '/psr/cache/src/CacheException.php', 'Psr\\Cache\\CacheException' => __DIR__ . '/..' . '/psr/cache/src/CacheException.php',
'Psr\\Cache\\CacheItemInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemInterface.php', 'Psr\\Cache\\CacheItemInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemInterface.php',
'Psr\\Cache\\CacheItemPoolInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemPoolInterface.php', 'Psr\\Cache\\CacheItemPoolInterface' => __DIR__ . '/..' . '/psr/cache/src/CacheItemPoolInterface.php',
@@ -9573,6 +9672,8 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'Termwind\\ValueObjects\\Node' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Node.php', 'Termwind\\ValueObjects\\Node' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Node.php',
'Termwind\\ValueObjects\\Style' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Style.php', 'Termwind\\ValueObjects\\Style' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Style.php',
'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php', 'Termwind\\ValueObjects\\Styles' => __DIR__ . '/..' . '/nunomaduro/termwind/src/ValueObjects/Styles.php',
'Tests\\Feature\\AjaxPostEndpointsTest' => __DIR__ . '/../..' . '/tests/Feature/AjaxPostEndpointsTest.php',
'Tests\\Feature\\CsrfExceptionsTest' => __DIR__ . '/../..' . '/tests/Feature/CsrfExceptionsTest.php',
'Tests\\Feature\\DashTest' => __DIR__ . '/../..' . '/tests/Feature/DashTest.php', 'Tests\\Feature\\DashTest' => __DIR__ . '/../..' . '/tests/Feature/DashTest.php',
'Tests\\Feature\\GetStatsTest' => __DIR__ . '/../..' . '/tests/Feature/GetStatsTest.php', 'Tests\\Feature\\GetStatsTest' => __DIR__ . '/../..' . '/tests/Feature/GetStatsTest.php',
'Tests\\Feature\\ImportTest' => __DIR__ . '/../..' . '/tests/Feature/ImportTest.php', 'Tests\\Feature\\ImportTest' => __DIR__ . '/../..' . '/tests/Feature/ImportTest.php',
@@ -9581,16 +9682,24 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'Tests\\Feature\\ItemExportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemExportTest.php', 'Tests\\Feature\\ItemExportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemExportTest.php',
'Tests\\Feature\\ItemImportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemImportTest.php', 'Tests\\Feature\\ItemImportTest' => __DIR__ . '/../..' . '/tests/Feature/ItemImportTest.php',
'Tests\\Feature\\ItemListTest' => __DIR__ . '/../..' . '/tests/Feature/ItemListTest.php', 'Tests\\Feature\\ItemListTest' => __DIR__ . '/../..' . '/tests/Feature/ItemListTest.php',
'Tests\\Feature\\ItemOwnershipTest' => __DIR__ . '/../..' . '/tests/Feature/ItemOwnershipTest.php',
'Tests\\Feature\\OrphanItemRecoveryTest' => __DIR__ . '/../..' . '/tests/Feature/OrphanItemRecoveryTest.php',
'Tests\\Feature\\RoutesRenderTest' => __DIR__ . '/../..' . '/tests/Feature/RoutesRenderTest.php',
'Tests\\Feature\\SearchTest' => __DIR__ . '/../..' . '/tests/Feature/SearchTest.php', 'Tests\\Feature\\SearchTest' => __DIR__ . '/../..' . '/tests/Feature/SearchTest.php',
'Tests\\Feature\\SettingsTest' => __DIR__ . '/../..' . '/tests/Feature/SettingsTest.php', 'Tests\\Feature\\SettingsTest' => __DIR__ . '/../..' . '/tests/Feature/SettingsTest.php',
'Tests\\Feature\\StorageDiskTest' => __DIR__ . '/../..' . '/tests/Feature/StorageDiskTest.php',
'Tests\\Feature\\TagListTest' => __DIR__ . '/../..' . '/tests/Feature/TagListTest.php', 'Tests\\Feature\\TagListTest' => __DIR__ . '/../..' . '/tests/Feature/TagListTest.php',
'Tests\\Feature\\TrustHostsTest' => __DIR__ . '/../..' . '/tests/Feature/TrustHostsTest.php', 'Tests\\Feature\\TrustHostsTest' => __DIR__ . '/../..' . '/tests/Feature/TrustHostsTest.php',
'Tests\\Feature\\TrustProxiesTest' => __DIR__ . '/../..' . '/tests/Feature/TrustProxiesTest.php', 'Tests\\Feature\\TrustProxiesTest' => __DIR__ . '/../..' . '/tests/Feature/TrustProxiesTest.php',
'Tests\\Feature\\UserDeleteItemsTest' => __DIR__ . '/../..' . '/tests/Feature/UserDeleteItemsTest.php',
'Tests\\Feature\\UserEditTest' => __DIR__ . '/../..' . '/tests/Feature/UserEditTest.php', 'Tests\\Feature\\UserEditTest' => __DIR__ . '/../..' . '/tests/Feature/UserEditTest.php',
'Tests\\Feature\\UserListTest' => __DIR__ . '/../..' . '/tests/Feature/UserListTest.php', 'Tests\\Feature\\UserListTest' => __DIR__ . '/../..' . '/tests/Feature/UserListTest.php',
'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php', 'Tests\\TestCase' => __DIR__ . '/../..' . '/tests/TestCase.php',
'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => __DIR__ . '/../..' . '/tests/Unit/database/seeders/SettingsSeederTest.php', 'Tests\\Unit\\database\\seeders\\SettingsSeederTest' => __DIR__ . '/../..' . '/tests/Unit/database/seeders/SettingsSeederTest.php',
'Tests\\Unit\\helpers\\ClassNameTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/ClassNameTest.php',
'Tests\\Unit\\helpers\\ColorHelpersTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/ColorHelpersTest.php',
'Tests\\Unit\\helpers\\IsImageTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/IsImageTest.php', 'Tests\\Unit\\helpers\\IsImageTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/IsImageTest.php',
'Tests\\Unit\\helpers\\SizeHelpersTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/SizeHelpersTest.php',
'Tests\\Unit\\helpers\\SlugTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/SlugTest.php', 'Tests\\Unit\\helpers\\SlugTest' => __DIR__ . '/../..' . '/tests/Unit/helpers/SlugTest.php',
'Tests\\Unit\\lang\\LangTest' => __DIR__ . '/../..' . '/tests/Unit/lang/LangTest.php', 'Tests\\Unit\\lang\\LangTest' => __DIR__ . '/../..' . '/tests/Unit/lang/LangTest.php',
'TheSeer\\Tokenizer\\Exception' => __DIR__ . '/..' . '/theseer/tokenizer/src/Exception.php', 'TheSeer\\Tokenizer\\Exception' => __DIR__ . '/..' . '/theseer/tokenizer/src/Exception.php',
@@ -9609,6 +9718,66 @@ class ComposerStaticInitb2555e5ff7197b9e020da74bbd3b7cfa
'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php', 'TijsVerkoyen\\CssToInlineStyles\\Css\\Rule\\Rule' => __DIR__ . '/..' . '/tijsverkoyen/css-to-inline-styles/src/Css/Rule/Rule.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
'WebSocket\\Client' => __DIR__ . '/..' . '/phrity/websocket/src/Client.php',
'WebSocket\\Configuration' => __DIR__ . '/..' . '/phrity/websocket/src/Configuration.php',
'WebSocket\\Connection' => __DIR__ . '/..' . '/phrity/websocket/src/Connection.php',
'WebSocket\\Constant' => __DIR__ . '/..' . '/phrity/websocket/src/Constant.php',
'WebSocket\\Exception\\BadOpcodeException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/BadOpcodeException.php',
'WebSocket\\Exception\\BadUriException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/BadUriException.php',
'WebSocket\\Exception\\ClientException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ClientException.php',
'WebSocket\\Exception\\CloseException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/CloseException.php',
'WebSocket\\Exception\\ConnectionClosedException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionClosedException.php',
'WebSocket\\Exception\\ConnectionFailureException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionFailureException.php',
'WebSocket\\Exception\\ConnectionLevelInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionLevelInterface.php',
'WebSocket\\Exception\\ConnectionTimeoutException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ConnectionTimeoutException.php',
'WebSocket\\Exception\\Exception' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/Exception.php',
'WebSocket\\Exception\\ExceptionInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ExceptionInterface.php',
'WebSocket\\Exception\\HandshakeException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/HandshakeException.php',
'WebSocket\\Exception\\MessageLevelInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/MessageLevelInterface.php',
'WebSocket\\Exception\\ReconnectException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ReconnectException.php',
'WebSocket\\Exception\\RunnerException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/RunnerException.php',
'WebSocket\\Exception\\ServerException' => __DIR__ . '/..' . '/phrity/websocket/src/Exception/ServerException.php',
'WebSocket\\Frame\\Frame' => __DIR__ . '/..' . '/phrity/websocket/src/Frame/Frame.php',
'WebSocket\\Frame\\FrameHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Frame/FrameHandler.php',
'WebSocket\\Http\\DefaultHttpFactory' => __DIR__ . '/..' . '/phrity/websocket/src/Http/DefaultHttpFactory.php',
'WebSocket\\Http\\HttpHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Http/HttpHandler.php',
'WebSocket\\Http\\Request' => __DIR__ . '/..' . '/phrity/websocket/src/Http/Request.php',
'WebSocket\\Http\\Response' => __DIR__ . '/..' . '/phrity/websocket/src/Http/Response.php',
'WebSocket\\Http\\ServerRequest' => __DIR__ . '/..' . '/phrity/websocket/src/Http/ServerRequest.php',
'WebSocket\\Message\\Binary' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Binary.php',
'WebSocket\\Message\\Close' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Close.php',
'WebSocket\\Message\\Message' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Message.php',
'WebSocket\\Message\\MessageHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Message/MessageHandler.php',
'WebSocket\\Message\\Ping' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Ping.php',
'WebSocket\\Message\\Pong' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Pong.php',
'WebSocket\\Message\\Text' => __DIR__ . '/..' . '/phrity/websocket/src/Message/Text.php',
'WebSocket\\Middleware\\Callback' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/Callback.php',
'WebSocket\\Middleware\\CloseHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CloseHandler.php',
'WebSocket\\Middleware\\CompressionExtension' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension.php',
'WebSocket\\Middleware\\CompressionExtension\\CompressorInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension/CompressorInterface.php',
'WebSocket\\Middleware\\CompressionExtension\\DeflateCompressor' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/CompressionExtension/DeflateCompressor.php',
'WebSocket\\Middleware\\FollowRedirect' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/FollowRedirect.php',
'WebSocket\\Middleware\\MiddlewareHandler' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/MiddlewareHandler.php',
'WebSocket\\Middleware\\MiddlewareInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/MiddlewareInterface.php',
'WebSocket\\Middleware\\PingInterval' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/PingInterval.php',
'WebSocket\\Middleware\\PingResponder' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/PingResponder.php',
'WebSocket\\Middleware\\ProcessHttpIncomingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpIncomingInterface.php',
'WebSocket\\Middleware\\ProcessHttpOutgoingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpOutgoingInterface.php',
'WebSocket\\Middleware\\ProcessHttpStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessHttpStack.php',
'WebSocket\\Middleware\\ProcessIncomingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessIncomingInterface.php',
'WebSocket\\Middleware\\ProcessOutgoingInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessOutgoingInterface.php',
'WebSocket\\Middleware\\ProcessStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessStack.php',
'WebSocket\\Middleware\\ProcessTickInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessTickInterface.php',
'WebSocket\\Middleware\\ProcessTickStack' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/ProcessTickStack.php',
'WebSocket\\Middleware\\SubprotocolNegotiation' => __DIR__ . '/..' . '/phrity/websocket/src/Middleware/SubprotocolNegotiation.php',
'WebSocket\\Runtime\\IdentityInterface' => __DIR__ . '/..' . '/phrity/websocket/src/Runtime/IdentityInterface.php',
'WebSocket\\Runtime\\Runner' => __DIR__ . '/..' . '/phrity/websocket/src/Runtime/Runner.php',
'WebSocket\\Server' => __DIR__ . '/..' . '/phrity/websocket/src/Server.php',
'WebSocket\\Trait\\ConfigurationTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/ConfigurationTrait.php',
'WebSocket\\Trait\\ListenerTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/ListenerTrait.php',
'WebSocket\\Trait\\OpcodeTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/OpcodeTrait.php',
'WebSocket\\Trait\\SendMethodsTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/SendMethodsTrait.php',
'WebSocket\\Trait\\StringableTrait' => __DIR__ . '/..' . '/phrity/websocket/src/Trait/StringableTrait.php',
'Whoops\\Exception\\ErrorException' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/ErrorException.php', 'Whoops\\Exception\\ErrorException' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/ErrorException.php',
'Whoops\\Exception\\Formatter' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Formatter.php', 'Whoops\\Exception\\Formatter' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Formatter.php',
'Whoops\\Exception\\Frame' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Frame.php', 'Whoops\\Exception\\Frame' => __DIR__ . '/..' . '/filp/whoops/src/Whoops/Exception/Frame.php',
+628
View File
@@ -4235,6 +4235,87 @@
], ],
"install-path": "../nunomaduro/termwind" "install-path": "../nunomaduro/termwind"
}, },
{
"name": "nyholm/psr7",
"version": "1.8.2",
"version_normalized": "1.8.2.0",
"source": {
"type": "git",
"url": "https://github.com/Nyholm/psr7.git",
"reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
"reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3",
"shasum": ""
},
"require": {
"php": ">=7.2",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"provide": {
"php-http/message-factory-implementation": "1.0",
"psr/http-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0"
},
"require-dev": {
"http-interop/http-factory-tests": "^0.9",
"php-http/message-factory": "^1.0",
"php-http/psr7-integration-tests": "^1.0",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
"symfony/error-handler": "^4.4"
},
"time": "2024-09-09T07:06:30+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Nyholm\\Psr7\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
},
{
"name": "Martijn van der Ven",
"email": "martijn@vanderven.se"
}
],
"description": "A fast PHP7 implementation of PSR-7",
"homepage": "https://tnyholm.se",
"keywords": [
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/Nyholm/psr7/issues",
"source": "https://github.com/Nyholm/psr7/tree/1.8.2"
},
"funding": [
{
"url": "https://github.com/Zegnat",
"type": "github"
},
{
"url": "https://github.com/nyholm",
"type": "github"
}
],
"install-path": "../nyholm/psr7"
},
{ {
"name": "phar-io/manifest", "name": "phar-io/manifest",
"version": "2.0.4", "version": "2.0.4",
@@ -5346,6 +5427,553 @@
], ],
"install-path": "../phpunit/phpunit" "install-path": "../phpunit/phpunit"
}, },
{
"name": "phrity/comparison",
"version": "1.4.1",
"version_normalized": "1.4.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-comparison.git",
"reference": "cf80abb822537eeaaeb4142157cd667ca6372a13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-comparison/zipball/cf80abb822537eeaaeb4142157cd667ca6372a13",
"reference": "cf80abb822537eeaaeb4142157cd667ca6372a13",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"time": "2025-12-05T07:38:30+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Comparison\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.",
"homepage": "https://phrity.sirn.se/comparison",
"keywords": [
"comparable",
"comparator",
"comparison",
"equalable",
"filter",
"sort"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-comparison/issues",
"source": "https://github.com/sirn-se/phrity-comparison/tree/1.4.1"
},
"install-path": "../phrity/comparison"
},
{
"name": "phrity/http",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-http.git",
"reference": "1e7eee67359287b94aae2b7d40b730d5f5394943"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-http/zipball/1e7eee67359287b94aae2b7d40b730d5f5394943",
"reference": "1e7eee67359287b94aae2b7d40b730d5f5394943",
"shasum": ""
},
"require": {
"php": "^8.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"guzzlehttp/psr7": "^2.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"time": "2025-12-22T20:22:29+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Http\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Utilities and interfaces for handling HTTP.",
"homepage": "https://phrity.sirn.se/http",
"keywords": [
"HTTP Factories",
"HTTP Serializer",
"http",
"psr-17",
"psr-7"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-http/issues",
"source": "https://github.com/sirn-se/phrity-http/tree/1.1.0"
},
"install-path": "../phrity/http"
},
{
"name": "phrity/net-stream",
"version": "2.4.0",
"version_normalized": "2.4.0.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-net-stream.git",
"reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-net-stream/zipball/a0726a8dddf11a4cd3a9e45d17e145d78e948f43",
"reference": "a0726a8dddf11a4cd3a9e45d17e145d78e948f43",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-errorhandler": "^1.1",
"phrity/util-interpolator": "^1.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/net-uri": "^2.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
},
"time": "2026-03-25T18:16:40+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory",
"homepage": "https://phrity.sirn.se/net-stream",
"keywords": [
"Socket",
"client",
"psr-17",
"psr-7",
"server",
"stream",
"stream factory"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-net-stream/issues",
"source": "https://github.com/sirn-se/phrity-net-stream/tree/2.4.0"
},
"install-path": "../phrity/net-stream"
},
{
"name": "phrity/net-uri",
"version": "2.2.1",
"version_normalized": "2.2.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-net-uri.git",
"reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-net-uri/zipball/0737de026b75177ae302ac9fdbbd0ffc2610f3b8",
"reference": "0737de026b75177ae302ac9fdbbd0ffc2610f3b8",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": "^8.1",
"phrity/comparison": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 | ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"suggest": {
"ext-intl": "Enables IDN conversion for non-ASCII domains"
},
"time": "2025-12-05T10:39:22+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "PSR-7 Uri and PSR-17 UriFactory implementation",
"homepage": "https://phrity.sirn.se/net-uri",
"keywords": [
"psr-17",
"psr-7",
"uri",
"uri factory"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-net-uri/issues",
"source": "https://github.com/sirn-se/phrity-net-uri/tree/2.2.1"
},
"install-path": "../phrity/net-uri"
},
{
"name": "phrity/util-accessor",
"version": "1.3.1",
"version_normalized": "1.3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-accessor.git",
"reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-accessor/zipball/e9741da6b532fa2da7f627fce60fa77f00f6e354",
"reference": "e9741da6b532fa2da7f627fce60fa77f00f6e354",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-transformer": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"time": "2025-12-05T21:18:15+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.",
"homepage": "https://phrity.sirn.se/util-accessor",
"keywords": [
"accessor"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-accessor/issues",
"source": "https://github.com/sirn-se/phrity-util-accessor/tree/1.3.1"
},
"install-path": "../phrity/util-accessor"
},
{
"name": "phrity/util-errorhandler",
"version": "1.2.2",
"version_normalized": "1.2.2.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-errorhandler.git",
"reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-errorhandler/zipball/70a669cc22db2eed6a109ec66fd95168a4332c9b",
"reference": "70a669cc22db2eed6a109ec66fd95168a4332c9b",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"time": "2025-12-05T21:25:36+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Inline error handler; catch and resolve errors for code block.",
"homepage": "https://phrity.sirn.se/util-errorhandler",
"keywords": [
"error",
"warning"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-errorhandler/issues",
"source": "https://github.com/sirn-se/phrity-util-errorhandler/tree/1.2.2"
},
"install-path": "../phrity/util-errorhandler"
},
{
"name": "phrity/util-interpolator",
"version": "1.1.1",
"version_normalized": "1.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-interpolator.git",
"reference": "18b0362d2aff60984c530808333c5e64c80c3e50"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-interpolator/zipball/18b0362d2aff60984c530808333c5e64c80c3e50",
"reference": "18b0362d2aff60984c530808333c5e64c80c3e50",
"shasum": ""
},
"require": {
"php": "^8.1",
"phrity/util-accessor": "^1.3",
"phrity/util-transformer": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"time": "2025-12-05T21:35:25+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Util\\Interpolator\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Interpolator class and trait.",
"homepage": "https://phrity.sirn.se/util-interpolator",
"keywords": [
"interpolate",
"interpolator"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-interpolator/issues",
"source": "https://github.com/sirn-se/phrity-util-interpolator/tree/1.1.1"
},
"install-path": "../phrity/util-interpolator"
},
{
"name": "phrity/util-transformer",
"version": "1.3.1",
"version_normalized": "1.3.1.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/phrity-util-transformer.git",
"reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/phrity-util-transformer/zipball/3fd4d7fa4077deafe5130c4b9ee8146b5488abe3",
"reference": "3fd4d7fa4077deafe5130c4b9ee8146b5488abe3",
"shasum": ""
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0",
"symfony/property-access": "^6.0 || ^7.0 || ^8.0",
"symfony/serializer": "^6.0 || ^7.0 || ^8.0",
"symfony/translation-contracts": "^3.0",
"symfony/uid": "^6.0 || ^7.0 || ^8.0"
},
"time": "2025-12-05T22:07:04+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Phrity\\Util\\Transformer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "Type transformers, normalizers and resolvers.",
"homepage": "https://phrity.sirn.se/util-transformer",
"keywords": [
"normalizer",
"resolver",
"transformer",
"type"
],
"support": {
"issues": "https://github.com/sirn-se/phrity-util-transformer/issues",
"source": "https://github.com/sirn-se/phrity-util-transformer/tree/1.3.1"
},
"install-path": "../phrity/util-transformer"
},
{
"name": "phrity/websocket",
"version": "3.7.3",
"version_normalized": "3.7.3.0",
"source": {
"type": "git",
"url": "https://github.com/sirn-se/websocket-php.git",
"reference": "c92d613ec298ea108b8ebae306609cd40427f7d6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sirn-se/websocket-php/zipball/c92d613ec298ea108b8ebae306609cd40427f7d6",
"reference": "c92d613ec298ea108b8ebae306609cd40427f7d6",
"shasum": ""
},
"require": {
"nyholm/psr7": "^1.4",
"php": "^8.1",
"phrity/http": "^1.1",
"phrity/net-stream": "^2.4",
"phrity/net-uri": "^2.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/logger-console": "^1.0",
"phrity/net-mock": "^2.4",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
},
"time": "2026-06-22T14:53:45+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WebSocket\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"description": "WebSocket client and server",
"homepage": "https://phrity.sirn.se/websocket",
"keywords": [
"client",
"server",
"websocket"
],
"support": {
"issues": "https://github.com/sirn-se/websocket-php/issues",
"source": "https://github.com/sirn-se/websocket-php/tree/3.7.3"
},
"install-path": "../phrity/websocket"
},
{ {
"name": "psr/cache", "name": "psr/cache",
"version": "3.0.0", "version": "3.0.0",
+92 -2
View File
@@ -3,7 +3,7 @@
'name' => 'laravel/laravel', 'name' => 'laravel/laravel',
'pretty_version' => '2.x-dev', 'pretty_version' => '2.x-dev',
'version' => '2.9999999.9999999.9999999-dev', 'version' => '2.9999999.9999999.9999999-dev',
'reference' => '39191885e53b2c8ab49b3d594ece9825b6fd87b4', 'reference' => '93a321ff5b77e698e52d0e204487a968df4b5fc9',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -508,7 +508,7 @@
'laravel/laravel' => array( 'laravel/laravel' => array(
'pretty_version' => '2.x-dev', 'pretty_version' => '2.x-dev',
'version' => '2.9999999.9999999.9999999-dev', 'version' => '2.9999999.9999999.9999999-dev',
'reference' => '39191885e53b2c8ab49b3d594ece9825b6fd87b4', 'reference' => '93a321ff5b77e698e52d0e204487a968df4b5fc9',
'type' => 'project', 'type' => 'project',
'install_path' => __DIR__ . '/../../', 'install_path' => __DIR__ . '/../../',
'aliases' => array(), 'aliases' => array(),
@@ -727,6 +727,15 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'nyholm/psr7' => array(
'pretty_version' => '1.8.2',
'version' => '1.8.2.0',
'reference' => 'a71f2b11690f4b24d099d6b16690a90ae14fc6f3',
'type' => 'library',
'install_path' => __DIR__ . '/../nyholm/psr7',
'aliases' => array(),
'dev_requirement' => false,
),
'phar-io/manifest' => array( 'phar-io/manifest' => array(
'pretty_version' => '2.0.4', 'pretty_version' => '2.0.4',
'version' => '2.0.4.0', 'version' => '2.0.4.0',
@@ -889,6 +898,87 @@
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => true, 'dev_requirement' => true,
), ),
'phrity/comparison' => array(
'pretty_version' => '1.4.1',
'version' => '1.4.1.0',
'reference' => 'cf80abb822537eeaaeb4142157cd667ca6372a13',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/comparison',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/http' => array(
'pretty_version' => '1.1.0',
'version' => '1.1.0.0',
'reference' => '1e7eee67359287b94aae2b7d40b730d5f5394943',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/http',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/net-stream' => array(
'pretty_version' => '2.4.0',
'version' => '2.4.0.0',
'reference' => 'a0726a8dddf11a4cd3a9e45d17e145d78e948f43',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/net-stream',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/net-uri' => array(
'pretty_version' => '2.2.1',
'version' => '2.2.1.0',
'reference' => '0737de026b75177ae302ac9fdbbd0ffc2610f3b8',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/net-uri',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/util-accessor' => array(
'pretty_version' => '1.3.1',
'version' => '1.3.1.0',
'reference' => 'e9741da6b532fa2da7f627fce60fa77f00f6e354',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/util-accessor',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/util-errorhandler' => array(
'pretty_version' => '1.2.2',
'version' => '1.2.2.0',
'reference' => '70a669cc22db2eed6a109ec66fd95168a4332c9b',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/util-errorhandler',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/util-interpolator' => array(
'pretty_version' => '1.1.1',
'version' => '1.1.1.0',
'reference' => '18b0362d2aff60984c530808333c5e64c80c3e50',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/util-interpolator',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/util-transformer' => array(
'pretty_version' => '1.3.1',
'version' => '1.3.1.0',
'reference' => '3fd4d7fa4077deafe5130c4b9ee8146b5488abe3',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/util-transformer',
'aliases' => array(),
'dev_requirement' => false,
),
'phrity/websocket' => array(
'pretty_version' => '3.7.3',
'version' => '3.7.3.0',
'reference' => 'c92d613ec298ea108b8ebae306609cd40427f7d6',
'type' => 'library',
'install_path' => __DIR__ . '/../phrity/websocket',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/cache' => array( 'psr/cache' => array(
'pretty_version' => '3.0.0', 'pretty_version' => '3.0.0',
'version' => '3.0.0.0', 'version' => '3.0.0.0',
+172
View File
@@ -0,0 +1,172 @@
# Changelog
All notable changes to this project will be documented in this file, in reverse chronological order by release.
## 1.8.2
- Fix deprecation warnings in PHP 8.4
## 1.8.1
- Fix error handling in Stream::getContents()
## 1.8.0
- Deprecate HttplugFactory, use Psr17Factory instead
- Make depencendy on php-http/message-factory optional
## 1.7.0
- Bump to PHP 7.2 minimum
- Allow psr/http-message v2
- Use copy-on-write for streams created from strings
## 1.6.1
- Security fix: CVE-2023-29197
## 1.6.0
### Changed
- Seek to the begining of the string when using Stream::create()
- Populate ServerRequest::getQueryParams() on instantiation
- Encode [reserved characters](https://www.rfc-editor.org/rfc/rfc3986#appendix-A) in userinfo in Uri
- Normalize leading slashes for Uri::getPath()
- Make Stream's constructor public
- Add some missing type checks on arguments
## 1.5.1
### Fixed
- Fixed deprecations on PHP 8.1
## 1.5.0
### Added
- Add explicit `@return mixed`
- Add explicit return types to HttplugFactory
### Fixed
- Improve error handling with streams
## 1.4.1
### Fixed
- `Psr17Factory::createStreamFromFile`, `UploadedFile::moveTo`, and
`UploadedFile::getStream` no longer throw `ValueError` in PHP 8.
## 1.4.0
### Removed
The `final` keyword was replaced by `@final` annotation.
## 1.3.2
### Fixed
- `Stream::read()` must not return boolean.
- Improved exception message when using wrong HTTP status code.
## 1.3.1
### Fixed
- Allow installation on PHP8
## 1.3.0
### Added
- Make Stream::__toString() compatible with throwing exceptions on PHP 7.4.
### Fixed
- Support for UTF-8 hostnames
- Support for numeric header names
## 1.2.1
### Changed
- Added `.github` and `phpstan.neon.dist` to `.gitattributes`.
## 1.2.0
### Changed
- Change minimal port number to 0 (unix socket)
- Updated `Psr17Factory::createResponse` to respect the specification. If second
argument is not used, a standard reason phrase. If an empty string is passed,
then the reason phrase will be empty.
### Fixed
- Check for seekable on the stream resource.
- Fixed the `Response::$reason` should never be null.
## 1.1.0
### Added
- Improved performance
- More tests for `UploadedFile` and `HttplugFactory`
### Removed
- Dead code
## 1.0.1
### Fixed
- Handle `fopen` failing in createStreamFromFile according to PSR-7.
- Reduce execution path to speed up performance.
- Fixed typos.
- Code style.
## 1.0.0
### Added
- Support for final PSR-17 (HTTP factories). (`Psr17Factory`)
- Support for numeric header values.
- Support for empty header values.
- All classes are final
- `HttplugFactory` that implements factory interfaces from HTTPlug.
### Changed
- `ServerRequest` does not extend `Request`.
### Removed
- The HTTPlug discovery strategy was removed since it is included in php-http/discovery 1.4.
- `UploadedFileFactory()` was removed in favor for `Psr17Factory`.
- `ServerRequestFactory()` was removed in favor for `Psr17Factory`.
- `StreamFactory`, `UriFactory`, abd `MessageFactory`. Use `HttplugFactory` instead.
- `ServerRequestFactory::createServerRequestFromArray`, `ServerRequestFactory::createServerRequestFromArrays` and
`ServerRequestFactory::createServerRequestFromGlobals`. Please use the new `nyholm/psr7-server` instead.
## 0.3.0
### Added
- Return types.
- Many `InvalidArgumentException`s are thrown when you use invalid arguments.
- Integration tests for `UploadedFile` and `ServerRequest`.
### Changed
- We dropped PHP7.0 support.
- PSR-17 factories have been marked as internal. They do not fall under our BC promise until PSR-17 is accepted.
- `UploadedFileFactory::createUploadedFile` does not accept a string file path.
## 0.2.3
No changelog before this release
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Tobias Nyholm
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+108
View File
@@ -0,0 +1,108 @@
# PSR-7 implementation
[![Latest Version](https://img.shields.io/github/release/Nyholm/psr7.svg?style=flat-square)](https://github.com/Nyholm/psr7/releases)
[![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7)
[![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Static analysis](https://github.com/Nyholm/psr7/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/static.yml?query=branch%3Amaster)
[![Tests](https://github.com/Nyholm/psr7/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/tests.yml?query=branch%3Amaster)
A super lightweight PSR-7 implementation. Very strict and very fast.
| Description | Guzzle | Laminas | Slim | Nyholm |
| ---- | ------ | ---- | ---- | ------ |
| Lines of code | 3.300 | 3.100 | 1.900 | 1.000 |
| PSR-7* | 66% | 100% | 75% | 100% |
| PSR-17 | No | Yes | Yes | Yes |
| HTTPlug | No | No | No | Yes |
| Performance (runs per second)** | 14.553 | 14.703 | 13.416 | 17.734 |
\* Percent of completed tests in https://github.com/php-http/psr7-integration-tests
\** Benchmark with 50.000 runs. See https://github.com/devanych/psr-http-benchmark (higher is better)
## Installation
```bash
composer require nyholm/psr7
```
If you are using Symfony Flex then you get all message factories registered as services.
## Usage
The PSR-7 objects do not contain any other public methods than those defined in
the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/).
### Create objects
Use the PSR-17 factory to create requests, streams, URIs etc.
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se');
$stream = $psr17Factory->createStream('foobar');
```
### Sending a request
With [HTTPlug](http://httplug.io/) or any other PSR-18 (HTTP client) you may send
requests like:
```bash
composer require kriswallsmith/buzz
```
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$psr18Client = new \Buzz\Client\Curl($psr17Factory);
$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se');
$response = $psr18Client->sendRequest($request);
```
### Create server requests
The [`nyholm/psr7-server`](https://github.com/Nyholm/psr7-server) package can be used
to create server requests from PHP superglobals.
```bash
composer require nyholm/psr7-server
```
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$creator = new \Nyholm\Psr7Server\ServerRequestCreator(
$psr17Factory, // ServerRequestFactory
$psr17Factory, // UriFactory
$psr17Factory, // UploadedFileFactory
$psr17Factory // StreamFactory
);
$serverRequest = $creator->fromGlobals();
```
### Emitting a response
```bash
composer require laminas/laminas-httphandlerrunner
```
```php
$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory();
$responseBody = $psr17Factory->createStream('Hello world');
$response = $psr17Factory->createResponse(200)->withBody($responseBody);
(new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response);
```
## Our goal
This package is currently maintained by [Tobias Nyholm](http://nyholm.se) and
[Martijn van der Ven](https://vanderven.se/martijn/). They have decided that the
goal of this library should be to provide a super strict implementation of
[PSR-7](https://www.php-fig.org/psr/psr-7/) that is blazing fast.
The package will never include any extra features nor helper methods. All our classes
and functions exist because they are required to fulfill the PSR-7 specification.
+49
View File
@@ -0,0 +1,49 @@
{
"name": "nyholm/psr7",
"description": "A fast PHP7 implementation of PSR-7",
"license": "MIT",
"keywords": ["psr-7", "psr-17"],
"homepage": "https://tnyholm.se",
"authors": [
{
"name": "Tobias Nyholm",
"email": "tobias.nyholm@gmail.com"
},
{
"name": "Martijn van der Ven",
"email": "martijn@vanderven.se"
}
],
"require": {
"php": ">=7.2",
"psr/http-message": "^1.1 || ^2.0",
"psr/http-factory": "^1.0"
},
"require-dev": {
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.4",
"php-http/message-factory": "^1.0",
"php-http/psr7-integration-tests": "^1.0",
"http-interop/http-factory-tests": "^0.9",
"symfony/error-handler": "^4.4"
},
"provide": {
"php-http/message-factory-implementation": "1.0",
"psr/http-message-implementation": "1.0",
"psr/http-factory-implementation": "1.0"
},
"autoload": {
"psr-4": {
"Nyholm\\Psr7\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\Nyholm\\Psr7\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.8-dev"
}
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7\Factory;
use Http\Message\{MessageFactory, StreamFactory, UriFactory};
use Nyholm\Psr7\{Request, Response, Stream, Uri};
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
if (!\interface_exists(MessageFactory::class)) {
throw new \LogicException('You cannot use "Nyholm\Psr7\Factory\HttplugFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead');
}
@\trigger_error('Class "Nyholm\Psr7\Factory\HttplugFactory" is deprecated since version 1.8, use "Nyholm\Psr7\Factory\Psr17Factory" instead.', \E_USER_DEPRECATED);
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*
* @deprecated since version 1.8, use Psr17Factory instead
*/
class HttplugFactory implements MessageFactory, StreamFactory, UriFactory
{
public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface
{
return new Request($method, $uri, $headers, $body, $protocolVersion);
}
public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1'): ResponseInterface
{
return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase);
}
public function createStream($body = null): StreamInterface
{
return Stream::create($body ?? '');
}
public function createUri($uri = ''): UriInterface
{
if ($uri instanceof UriInterface) {
return $uri;
}
return new Uri($uri);
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7\Factory;
use Nyholm\Psr7\{Request, Response, ServerRequest, Stream, UploadedFile, Uri};
use Psr\Http\Message\{RequestFactoryInterface, RequestInterface, ResponseFactoryInterface, ResponseInterface, ServerRequestFactoryInterface, ServerRequestInterface, StreamFactoryInterface, StreamInterface, UploadedFileFactoryInterface, UploadedFileInterface, UriFactoryInterface, UriInterface};
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface
{
public function createRequest(string $method, $uri): RequestInterface
{
return new Request($method, $uri);
}
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
if (2 > \func_num_args()) {
// This will make the Response class to use a custom reasonPhrase
$reasonPhrase = null;
}
return new Response($code, [], null, '1.1', $reasonPhrase);
}
public function createStream(string $content = ''): StreamInterface
{
return Stream::create($content);
}
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
if ('' === $filename) {
throw new \RuntimeException('Path cannot be empty');
}
if (false === $resource = @\fopen($filename, $mode)) {
if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) {
throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode));
}
throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? ''));
}
return Stream::create($resource);
}
public function createStreamFromResource($resource): StreamInterface
{
return Stream::create($resource);
}
public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface
{
if (null === $size) {
$size = $stream->getSize();
}
return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType);
}
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface
{
return new ServerRequest($method, $uri, [], null, '1.1', $serverParams);
}
}
+235
View File
@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\StreamInterface;
/**
* Trait implementing functionality common to requests and responses.
*
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
*/
trait MessageTrait
{
/** @var array Map of all registered headers, as original name => array of values */
private $headers = [];
/** @var array Map of lowercase header name => original name at registration */
private $headerNames = [];
/** @var string */
private $protocol = '1.1';
/** @var StreamInterface|null */
private $stream;
public function getProtocolVersion(): string
{
return $this->protocol;
}
/**
* @return static
*/
public function withProtocolVersion($version): MessageInterface
{
if (!\is_scalar($version)) {
throw new \InvalidArgumentException('Protocol version must be a string');
}
if ($this->protocol === $version) {
return $this;
}
$new = clone $this;
$new->protocol = (string) $version;
return $new;
}
public function getHeaders(): array
{
return $this->headers;
}
public function hasHeader($header): bool
{
return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]);
}
public function getHeader($header): array
{
if (!\is_string($header)) {
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
}
$header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
if (!isset($this->headerNames[$header])) {
return [];
}
$header = $this->headerNames[$header];
return $this->headers[$header];
}
public function getHeaderLine($header): string
{
return \implode(', ', $this->getHeader($header));
}
/**
* @return static
*/
public function withHeader($header, $value): MessageInterface
{
$value = $this->validateAndTrimHeader($header, $value);
$normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
$new = clone $this;
if (isset($new->headerNames[$normalized])) {
unset($new->headers[$new->headerNames[$normalized]]);
}
$new->headerNames[$normalized] = $header;
$new->headers[$header] = $value;
return $new;
}
/**
* @return static
*/
public function withAddedHeader($header, $value): MessageInterface
{
if (!\is_string($header) || '' === $header) {
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
}
$new = clone $this;
$new->setHeaders([$header => $value]);
return $new;
}
/**
* @return static
*/
public function withoutHeader($header): MessageInterface
{
if (!\is_string($header)) {
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
}
$normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
if (!isset($this->headerNames[$normalized])) {
return $this;
}
$header = $this->headerNames[$normalized];
$new = clone $this;
unset($new->headers[$header], $new->headerNames[$normalized]);
return $new;
}
public function getBody(): StreamInterface
{
if (null === $this->stream) {
$this->stream = Stream::create('');
}
return $this->stream;
}
/**
* @return static
*/
public function withBody(StreamInterface $body): MessageInterface
{
if ($body === $this->stream) {
return $this;
}
$new = clone $this;
$new->stream = $body;
return $new;
}
private function setHeaders(array $headers): void
{
foreach ($headers as $header => $value) {
if (\is_int($header)) {
// If a header name was set to a numeric string, PHP will cast the key to an int.
// We must cast it back to a string in order to comply with validation.
$header = (string) $header;
}
$value = $this->validateAndTrimHeader($header, $value);
$normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
if (isset($this->headerNames[$normalized])) {
$header = $this->headerNames[$normalized];
$this->headers[$header] = \array_merge($this->headers[$header], $value);
} else {
$this->headerNames[$normalized] = $header;
$this->headers[$header] = $value;
}
}
}
/**
* Make sure the header complies with RFC 7230.
*
* Header names must be a non-empty string consisting of token characters.
*
* Header values must be strings consisting of visible characters with all optional
* leading and trailing whitespace stripped. This method will always strip such
* optional whitespace. Note that the method does not allow folding whitespace within
* the values as this was deprecated for almost all instances by the RFC.
*
* header-field = field-name ":" OWS field-value OWS
* field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^"
* / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) )
* OWS = *( SP / HTAB )
* field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] )
*
* @see https://tools.ietf.org/html/rfc7230#section-3.2.4
*/
private function validateAndTrimHeader($header, $values): array
{
if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) {
throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string');
}
if (!\is_array($values)) {
// This is simple, just one value.
if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) {
throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
}
return [\trim((string) $values, " \t")];
}
if (empty($values)) {
throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given');
}
// Assert Non empty array
$returnValues = [];
foreach ($values as $v) {
if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@D", (string) $v)) {
throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings');
}
$returnValues[] = \trim((string) $v, " \t");
}
return $returnValues;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\{RequestInterface, StreamInterface, UriInterface};
/**
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class Request implements RequestInterface
{
use MessageTrait;
use RequestTrait;
/**
* @param string $method HTTP method
* @param string|UriInterface $uri URI
* @param array $headers Request headers
* @param string|resource|StreamInterface|null $body Request body
* @param string $version Protocol version
*/
public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1')
{
if (!($uri instanceof UriInterface)) {
$uri = new Uri($uri);
}
$this->method = $method;
$this->uri = $uri;
$this->setHeaders($headers);
$this->protocol = $version;
if (!$this->hasHeader('Host')) {
$this->updateHostFromUri();
}
// If we got no body, defer initialization of the stream until Request::getBody()
if ('' !== $body && null !== $body) {
$this->stream = Stream::create($body);
}
}
}
+127
View File
@@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\UriInterface;
/**
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise
*/
trait RequestTrait
{
/** @var string */
private $method;
/** @var string|null */
private $requestTarget;
/** @var UriInterface|null */
private $uri;
public function getRequestTarget(): string
{
if (null !== $this->requestTarget) {
return $this->requestTarget;
}
if ('' === $target = $this->uri->getPath()) {
$target = '/';
}
if ('' !== $this->uri->getQuery()) {
$target .= '?' . $this->uri->getQuery();
}
return $target;
}
/**
* @return static
*/
public function withRequestTarget($requestTarget): RequestInterface
{
if (!\is_string($requestTarget)) {
throw new \InvalidArgumentException('Request target must be a string');
}
if (\preg_match('#\s#', $requestTarget)) {
throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace');
}
$new = clone $this;
$new->requestTarget = $requestTarget;
return $new;
}
public function getMethod(): string
{
return $this->method;
}
/**
* @return static
*/
public function withMethod($method): RequestInterface
{
if (!\is_string($method)) {
throw new \InvalidArgumentException('Method must be a string');
}
$new = clone $this;
$new->method = $method;
return $new;
}
public function getUri(): UriInterface
{
return $this->uri;
}
/**
* @return static
*/
public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface
{
if ($uri === $this->uri) {
return $this;
}
$new = clone $this;
$new->uri = $uri;
if (!$preserveHost || !$this->hasHeader('Host')) {
$new->updateHostFromUri();
}
return $new;
}
private function updateHostFromUri(): void
{
if ('' === $host = $this->uri->getHost()) {
return;
}
if (null !== ($port = $this->uri->getPort())) {
$host .= ':' . $port;
}
if (isset($this->headerNames['host'])) {
$header = $this->headerNames['host'];
} else {
$this->headerNames['host'] = $header = 'Host';
}
// Ensure Host is the first header.
// See: http://tools.ietf.org/html/rfc7230#section-5.4
$this->headers = [$header => [$host]] + $this->headers;
}
}
+93
View File
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\{ResponseInterface, StreamInterface};
/**
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class Response implements ResponseInterface
{
use MessageTrait;
/** @var array Map of standard HTTP status code/reason phrases */
private const PHRASES = [
100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing',
200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-status', 208 => 'Already Reported',
300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect',
400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Time-out', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Large', 415 => 'Unsupported Media Type', 416 => 'Requested range not satisfiable', 417 => 'Expectation Failed', 418 => 'I\'m a teapot', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 425 => 'Unordered Collection', 426 => 'Upgrade Required', 428 => 'Precondition Required', 429 => 'Too Many Requests', 431 => 'Request Header Fields Too Large', 451 => 'Unavailable For Legal Reasons',
500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Time-out', 505 => 'HTTP Version not supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', 511 => 'Network Authentication Required',
];
/** @var string */
private $reasonPhrase = '';
/** @var int */
private $statusCode;
/**
* @param int $status Status code
* @param array $headers Response headers
* @param string|resource|StreamInterface|null $body Response body
* @param string $version Protocol version
* @param string|null $reason Reason phrase (when empty a default will be used based on the status code)
*/
public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null)
{
// If we got no body, defer initialization of the stream until Response::getBody()
if ('' !== $body && null !== $body) {
$this->stream = Stream::create($body);
}
$this->statusCode = $status;
$this->setHeaders($headers);
if (null === $reason && isset(self::PHRASES[$this->statusCode])) {
$this->reasonPhrase = self::PHRASES[$status];
} else {
$this->reasonPhrase = $reason ?? '';
}
$this->protocol = $version;
}
public function getStatusCode(): int
{
return $this->statusCode;
}
public function getReasonPhrase(): string
{
return $this->reasonPhrase;
}
/**
* @return static
*/
public function withStatus($code, $reasonPhrase = ''): ResponseInterface
{
if (!\is_int($code) && !\is_string($code)) {
throw new \InvalidArgumentException('Status code has to be an integer');
}
$code = (int) $code;
if ($code < 100 || $code > 599) {
throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code));
}
$new = clone $this;
$new->statusCode = $code;
if ((null === $reasonPhrase || '' === $reasonPhrase) && isset(self::PHRASES[$new->statusCode])) {
$reasonPhrase = self::PHRASES[$new->statusCode];
}
$new->reasonPhrase = $reasonPhrase;
return $new;
}
}
+201
View File
@@ -0,0 +1,201 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\{ServerRequestInterface, StreamInterface, UploadedFileInterface, UriInterface};
/**
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class ServerRequest implements ServerRequestInterface
{
use MessageTrait;
use RequestTrait;
/** @var array */
private $attributes = [];
/** @var array */
private $cookieParams = [];
/** @var array|object|null */
private $parsedBody;
/** @var array */
private $queryParams = [];
/** @var array */
private $serverParams;
/** @var UploadedFileInterface[] */
private $uploadedFiles = [];
/**
* @param string $method HTTP method
* @param string|UriInterface $uri URI
* @param array $headers Request headers
* @param string|resource|StreamInterface|null $body Request body
* @param string $version Protocol version
* @param array $serverParams Typically the $_SERVER superglobal
*/
public function __construct(string $method, $uri, array $headers = [], $body = null, string $version = '1.1', array $serverParams = [])
{
$this->serverParams = $serverParams;
if (!($uri instanceof UriInterface)) {
$uri = new Uri($uri);
}
$this->method = $method;
$this->uri = $uri;
$this->setHeaders($headers);
$this->protocol = $version;
\parse_str($uri->getQuery(), $this->queryParams);
if (!$this->hasHeader('Host')) {
$this->updateHostFromUri();
}
// If we got no body, defer initialization of the stream until ServerRequest::getBody()
if ('' !== $body && null !== $body) {
$this->stream = Stream::create($body);
}
}
public function getServerParams(): array
{
return $this->serverParams;
}
public function getUploadedFiles(): array
{
return $this->uploadedFiles;
}
/**
* @return static
*/
public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface
{
$new = clone $this;
$new->uploadedFiles = $uploadedFiles;
return $new;
}
public function getCookieParams(): array
{
return $this->cookieParams;
}
/**
* @return static
*/
public function withCookieParams(array $cookies): ServerRequestInterface
{
$new = clone $this;
$new->cookieParams = $cookies;
return $new;
}
public function getQueryParams(): array
{
return $this->queryParams;
}
/**
* @return static
*/
public function withQueryParams(array $query): ServerRequestInterface
{
$new = clone $this;
$new->queryParams = $query;
return $new;
}
/**
* @return array|object|null
*/
public function getParsedBody()
{
return $this->parsedBody;
}
/**
* @return static
*/
public function withParsedBody($data): ServerRequestInterface
{
if (!\is_array($data) && !\is_object($data) && null !== $data) {
throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null');
}
$new = clone $this;
$new->parsedBody = $data;
return $new;
}
public function getAttributes(): array
{
return $this->attributes;
}
/**
* @return mixed
*/
public function getAttribute($attribute, $default = null)
{
if (!\is_string($attribute)) {
throw new \InvalidArgumentException('Attribute name must be a string');
}
if (false === \array_key_exists($attribute, $this->attributes)) {
return $default;
}
return $this->attributes[$attribute];
}
/**
* @return static
*/
public function withAttribute($attribute, $value): ServerRequestInterface
{
if (!\is_string($attribute)) {
throw new \InvalidArgumentException('Attribute name must be a string');
}
$new = clone $this;
$new->attributes[$attribute] = $value;
return $new;
}
/**
* @return static
*/
public function withoutAttribute($attribute): ServerRequestInterface
{
if (!\is_string($attribute)) {
throw new \InvalidArgumentException('Attribute name must be a string');
}
if (false === \array_key_exists($attribute, $this->attributes)) {
return $this;
}
$new = clone $this;
unset($new->attributes[$attribute]);
return $new;
}
}
+399
View File
@@ -0,0 +1,399 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\StreamInterface;
/**
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class Stream implements StreamInterface
{
use StreamTrait;
/** @var resource|null A resource reference */
private $stream;
/** @var bool */
private $seekable;
/** @var bool */
private $readable;
/** @var bool */
private $writable;
/** @var array|mixed|void|bool|null */
private $uri;
/** @var int|null */
private $size;
/** @var array Hash of readable and writable stream types */
private const READ_WRITE_HASH = [
'read' => [
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a+' => true,
],
'write' => [
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
],
];
/**
* @param resource $body
*/
public function __construct($body)
{
if (!\is_resource($body)) {
throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource');
}
$this->stream = $body;
$meta = \stream_get_meta_data($this->stream);
$this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR);
$this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
$this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
}
/**
* Creates a new PSR-7 stream.
*
* @param string|resource|StreamInterface $body
*
* @throws \InvalidArgumentException
*/
public static function create($body = ''): StreamInterface
{
if ($body instanceof StreamInterface) {
return $body;
}
if (\is_string($body)) {
if (200000 <= \strlen($body)) {
$body = self::openZvalStream($body);
} else {
$resource = \fopen('php://memory', 'r+');
\fwrite($resource, $body);
\fseek($resource, 0);
$body = $resource;
}
}
if (!\is_resource($body)) {
throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface');
}
return new self($body);
}
/**
* Closes the stream when the destructed.
*/
public function __destruct()
{
$this->close();
}
public function close(): void
{
if (isset($this->stream)) {
if (\is_resource($this->stream)) {
\fclose($this->stream);
}
$this->detach();
}
}
public function detach()
{
if (!isset($this->stream)) {
return null;
}
$result = $this->stream;
unset($this->stream);
$this->size = $this->uri = null;
$this->readable = $this->writable = $this->seekable = false;
return $result;
}
private function getUri()
{
if (false !== $this->uri) {
$this->uri = $this->getMetadata('uri') ?? false;
}
return $this->uri;
}
public function getSize(): ?int
{
if (null !== $this->size) {
return $this->size;
}
if (!isset($this->stream)) {
return null;
}
// Clear the stat cache if the stream has a URI
if ($uri = $this->getUri()) {
\clearstatcache(true, $uri);
}
$stats = \fstat($this->stream);
if (isset($stats['size'])) {
$this->size = $stats['size'];
return $this->size;
}
return null;
}
public function tell(): int
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (false === $result = @\ftell($this->stream)) {
throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? ''));
}
return $result;
}
public function eof(): bool
{
return !isset($this->stream) || \feof($this->stream);
}
public function isSeekable(): bool
{
return $this->seekable;
}
public function seek($offset, $whence = \SEEK_SET): void
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->seekable) {
throw new \RuntimeException('Stream is not seekable');
}
if (-1 === \fseek($this->stream, $offset, $whence)) {
throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, true));
}
}
public function rewind(): void
{
$this->seek(0);
}
public function isWritable(): bool
{
return $this->writable;
}
public function write($string): int
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->writable) {
throw new \RuntimeException('Cannot write to a non-writable stream');
}
// We can't know the size after writing anything
$this->size = null;
if (false === $result = @\fwrite($this->stream, $string)) {
throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? ''));
}
return $result;
}
public function isReadable(): bool
{
return $this->readable;
}
public function read($length): string
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
if (!$this->readable) {
throw new \RuntimeException('Cannot read from non-readable stream');
}
if (false === $result = @\fread($this->stream, $length)) {
throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? ''));
}
return $result;
}
public function getContents(): string
{
if (!isset($this->stream)) {
throw new \RuntimeException('Stream is detached');
}
$exception = null;
\set_error_handler(static function ($type, $message) use (&$exception) {
throw $exception = new \RuntimeException('Unable to read stream contents: ' . $message);
});
try {
return \stream_get_contents($this->stream);
} catch (\Throwable $e) {
throw $e === $exception ? $e : new \RuntimeException('Unable to read stream contents: ' . $e->getMessage(), 0, $e);
} finally {
\restore_error_handler();
}
}
/**
* @return mixed
*/
public function getMetadata($key = null)
{
if (null !== $key && !\is_string($key)) {
throw new \InvalidArgumentException('Metadata key must be a string');
}
if (!isset($this->stream)) {
return $key ? null : [];
}
$meta = \stream_get_meta_data($this->stream);
if (null === $key) {
return $meta;
}
return $meta[$key] ?? null;
}
private static function openZvalStream(string $body)
{
static $wrapper;
$wrapper ?? \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper = \get_class(new class() {
public $context;
private $data;
private $position = 0;
public function stream_open(): bool
{
$this->data = \stream_context_get_options($this->context)['Nyholm-Psr7-Zval']['data'];
\stream_context_set_option($this->context, 'Nyholm-Psr7-Zval', 'data', null);
return true;
}
public function stream_read(int $count): string
{
$result = \substr($this->data, $this->position, $count);
$this->position += \strlen($result);
return $result;
}
public function stream_write(string $data): int
{
$this->data = \substr_replace($this->data, $data, $this->position, \strlen($data));
$this->position += \strlen($data);
return \strlen($data);
}
public function stream_tell(): int
{
return $this->position;
}
public function stream_eof(): bool
{
return \strlen($this->data) <= $this->position;
}
public function stream_stat(): array
{
return [
'mode' => 33206, // POSIX_S_IFREG | 0666
'nlink' => 1,
'rdev' => -1,
'size' => \strlen($this->data),
'blksize' => -1,
'blocks' => -1,
];
}
public function stream_seek(int $offset, int $whence): bool
{
if (\SEEK_SET === $whence && (0 <= $offset && \strlen($this->data) >= $offset)) {
$this->position = $offset;
} elseif (\SEEK_CUR === $whence && 0 <= $offset) {
$this->position += $offset;
} elseif (\SEEK_END === $whence && (0 > $offset && 0 <= $offset = \strlen($this->data) + $offset)) {
$this->position = $offset;
} else {
return false;
}
return true;
}
public function stream_set_option(): bool
{
return true;
}
public function stream_truncate(int $new_size): bool
{
if ($new_size) {
$this->data = \substr($this->data, 0, $new_size);
$this->position = \min($this->position, $new_size);
} else {
$this->data = '';
$this->position = 0;
}
return true;
}
}));
$context = \stream_context_create(['Nyholm-Psr7-Zval' => ['data' => $body]]);
if (!$stream = @\fopen('Nyholm-Psr7-Zval://', 'r+', false, $context)) {
\stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper);
$stream = \fopen('Nyholm-Psr7-Zval://', 'r+', false, $context);
}
return $stream;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\StreamInterface;
use Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler;
use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler;
if (\PHP_VERSION_ID >= 70400 || (new \ReflectionMethod(StreamInterface::class, '__toString'))->hasReturnType()) {
/**
* @internal
*/
trait StreamTrait
{
public function __toString(): string
{
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
}
}
} else {
/**
* @internal
*/
trait StreamTrait
{
/**
* @return string
*/
public function __toString()
{
try {
if ($this->isSeekable()) {
$this->seek(0);
}
return $this->getContents();
} catch (\Throwable $e) {
if (\is_array($errorHandler = \set_error_handler('var_dump'))) {
$errorHandler = $errorHandler[0] ?? null;
}
\restore_error_handler();
if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) {
return \trigger_error((string) $e, \E_USER_ERROR);
}
return '';
}
}
}
}
+179
View File
@@ -0,0 +1,179 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
/**
* @author Michael Dowling and contributors to guzzlehttp/psr7
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class UploadedFile implements UploadedFileInterface
{
/** @var array */
private const ERRORS = [
\UPLOAD_ERR_OK => 1,
\UPLOAD_ERR_INI_SIZE => 1,
\UPLOAD_ERR_FORM_SIZE => 1,
\UPLOAD_ERR_PARTIAL => 1,
\UPLOAD_ERR_NO_FILE => 1,
\UPLOAD_ERR_NO_TMP_DIR => 1,
\UPLOAD_ERR_CANT_WRITE => 1,
\UPLOAD_ERR_EXTENSION => 1,
];
/** @var string */
private $clientFilename;
/** @var string */
private $clientMediaType;
/** @var int */
private $error;
/** @var string|null */
private $file;
/** @var bool */
private $moved = false;
/** @var int */
private $size;
/** @var StreamInterface|null */
private $stream;
/**
* @param StreamInterface|string|resource $streamOrFile
* @param int $size
* @param int $errorStatus
* @param string|null $clientFilename
* @param string|null $clientMediaType
*/
public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null)
{
if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) {
throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants');
}
if (false === \is_int($size)) {
throw new \InvalidArgumentException('Upload file size must be an integer');
}
if (null !== $clientFilename && !\is_string($clientFilename)) {
throw new \InvalidArgumentException('Upload file client filename must be a string or null');
}
if (null !== $clientMediaType && !\is_string($clientMediaType)) {
throw new \InvalidArgumentException('Upload file client media type must be a string or null');
}
$this->error = $errorStatus;
$this->size = $size;
$this->clientFilename = $clientFilename;
$this->clientMediaType = $clientMediaType;
if (\UPLOAD_ERR_OK === $this->error) {
// Depending on the value set file or stream variable.
if (\is_string($streamOrFile) && '' !== $streamOrFile) {
$this->file = $streamOrFile;
} elseif (\is_resource($streamOrFile)) {
$this->stream = Stream::create($streamOrFile);
} elseif ($streamOrFile instanceof StreamInterface) {
$this->stream = $streamOrFile;
} else {
throw new \InvalidArgumentException('Invalid stream or file provided for UploadedFile');
}
}
}
/**
* @throws \RuntimeException if is moved or not ok
*/
private function validateActive(): void
{
if (\UPLOAD_ERR_OK !== $this->error) {
throw new \RuntimeException('Cannot retrieve stream due to upload error');
}
if ($this->moved) {
throw new \RuntimeException('Cannot retrieve stream after it has already been moved');
}
}
public function getStream(): StreamInterface
{
$this->validateActive();
if ($this->stream instanceof StreamInterface) {
return $this->stream;
}
if (false === $resource = @\fopen($this->file, 'r')) {
throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? ''));
}
return Stream::create($resource);
}
public function moveTo($targetPath): void
{
$this->validateActive();
if (!\is_string($targetPath) || '' === $targetPath) {
throw new \InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
}
if (null !== $this->file) {
$this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath);
if (false === $this->moved) {
throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? ''));
}
} else {
$stream = $this->getStream();
if ($stream->isSeekable()) {
$stream->rewind();
}
if (false === $resource = @\fopen($targetPath, 'w')) {
throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? ''));
}
$dest = Stream::create($resource);
while (!$stream->eof()) {
if (!$dest->write($stream->read(1048576))) {
break;
}
}
$this->moved = true;
}
}
public function getSize(): int
{
return $this->size;
}
public function getError(): int
{
return $this->error;
}
public function getClientFilename(): ?string
{
return $this->clientFilename;
}
public function getClientMediaType(): ?string
{
return $this->clientMediaType;
}
}
+356
View File
@@ -0,0 +1,356 @@
<?php
declare(strict_types=1);
namespace Nyholm\Psr7;
use Psr\Http\Message\UriInterface;
/**
* PSR-7 URI implementation.
*
* @author Michael Dowling
* @author Tobias Schultze
* @author Matthew Weier O'Phinney
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
* @author Martijn van der Ven <martijn@vanderven.se>
*
* @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md
*/
class Uri implements UriInterface
{
private const SCHEMES = ['http' => 80, 'https' => 443];
private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~';
private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
private const CHAR_GEN_DELIMS = ':\/\?#\[\]@';
/** @var string Uri scheme. */
private $scheme = '';
/** @var string Uri user info. */
private $userInfo = '';
/** @var string Uri host. */
private $host = '';
/** @var int|null Uri port. */
private $port;
/** @var string Uri path. */
private $path = '';
/** @var string Uri query string. */
private $query = '';
/** @var string Uri fragment. */
private $fragment = '';
public function __construct(string $uri = '')
{
if ('' !== $uri) {
if (false === $parts = \parse_url($uri)) {
throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri));
}
// Apply parse_url parts to a URI.
$this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : '';
$this->userInfo = $parts['user'] ?? '';
$this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : '';
$this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null;
$this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : '';
$this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : '';
$this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : '';
if (isset($parts['pass'])) {
$this->userInfo .= ':' . $parts['pass'];
}
}
}
public function __toString(): string
{
return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment);
}
public function getScheme(): string
{
return $this->scheme;
}
public function getAuthority(): string
{
if ('' === $this->host) {
return '';
}
$authority = $this->host;
if ('' !== $this->userInfo) {
$authority = $this->userInfo . '@' . $authority;
}
if (null !== $this->port) {
$authority .= ':' . $this->port;
}
return $authority;
}
public function getUserInfo(): string
{
return $this->userInfo;
}
public function getHost(): string
{
return $this->host;
}
public function getPort(): ?int
{
return $this->port;
}
public function getPath(): string
{
$path = $this->path;
if ('' !== $path && '/' !== $path[0]) {
if ('' !== $this->host) {
// If the path is rootless and an authority is present, the path MUST be prefixed by "/"
$path = '/' . $path;
}
} elseif (isset($path[1]) && '/' === $path[1]) {
// If the path is starting with more than one "/", the
// starting slashes MUST be reduced to one.
$path = '/' . \ltrim($path, '/');
}
return $path;
}
public function getQuery(): string
{
return $this->query;
}
public function getFragment(): string
{
return $this->fragment;
}
/**
* @return static
*/
public function withScheme($scheme): UriInterface
{
if (!\is_string($scheme)) {
throw new \InvalidArgumentException('Scheme must be a string');
}
if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
return $this;
}
$new = clone $this;
$new->scheme = $scheme;
$new->port = $new->filterPort($new->port);
return $new;
}
/**
* @return static
*/
public function withUserInfo($user, $password = null): UriInterface
{
if (!\is_string($user)) {
throw new \InvalidArgumentException('User must be a string');
}
$info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user);
if (null !== $password && '' !== $password) {
if (!\is_string($password)) {
throw new \InvalidArgumentException('Password must be a string');
}
$info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password);
}
if ($this->userInfo === $info) {
return $this;
}
$new = clone $this;
$new->userInfo = $info;
return $new;
}
/**
* @return static
*/
public function withHost($host): UriInterface
{
if (!\is_string($host)) {
throw new \InvalidArgumentException('Host must be a string');
}
if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) {
return $this;
}
$new = clone $this;
$new->host = $host;
return $new;
}
/**
* @return static
*/
public function withPort($port): UriInterface
{
if ($this->port === $port = $this->filterPort($port)) {
return $this;
}
$new = clone $this;
$new->port = $port;
return $new;
}
/**
* @return static
*/
public function withPath($path): UriInterface
{
if ($this->path === $path = $this->filterPath($path)) {
return $this;
}
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* @return static
*/
public function withQuery($query): UriInterface
{
if ($this->query === $query = $this->filterQueryAndFragment($query)) {
return $this;
}
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* @return static
*/
public function withFragment($fragment): UriInterface
{
if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) {
return $this;
}
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* Create a URI string from its various parts.
*/
private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string
{
$uri = '';
if ('' !== $scheme) {
$uri .= $scheme . ':';
}
if ('' !== $authority) {
$uri .= '//' . $authority;
}
if ('' !== $path) {
if ('/' !== $path[0]) {
if ('' !== $authority) {
// If the path is rootless and an authority is present, the path MUST be prefixed by "/"
$path = '/' . $path;
}
} elseif (isset($path[1]) && '/' === $path[1]) {
if ('' === $authority) {
// If the path is starting with more than one "/" and no authority is present, the
// starting slashes MUST be reduced to one.
$path = '/' . \ltrim($path, '/');
}
}
$uri .= $path;
}
if ('' !== $query) {
$uri .= '?' . $query;
}
if ('' !== $fragment) {
$uri .= '#' . $fragment;
}
return $uri;
}
/**
* Is a given port non-standard for the current scheme?
*/
private static function isNonStandardPort(string $scheme, int $port): bool
{
return !isset(self::SCHEMES[$scheme]) || $port !== self::SCHEMES[$scheme];
}
private function filterPort($port): ?int
{
if (null === $port) {
return null;
}
$port = (int) $port;
if (0 > $port || 0xFFFF < $port) {
throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port));
}
return self::isNonStandardPort($this->scheme, $port) ? $port : null;
}
private function filterPath($path): string
{
if (!\is_string($path)) {
throw new \InvalidArgumentException('Path must be a string');
}
return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path);
}
private function filterQueryAndFragment($str): string
{
if (!\is_string($str)) {
throw new \InvalidArgumentException('Query and fragment must be a string');
}
return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str);
}
private static function rawurlencodeMatchZero(array $match): string
{
return \rawurlencode($match[0]);
}
}
+34
View File
@@ -0,0 +1,34 @@
{
"name": "phrity/comparison",
"type": "library",
"description": "Interfaces and helper trait for comparing objects. Comparator for sort and filter applications.",
"homepage": "https://phrity.sirn.se/comparison",
"keywords": ["comparison", "equalable", "comparable", "comparator", "sort", "filter"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Comparison\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Mock\\": "tests/Mock/"
}
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
/**
* File for Comparable inteface.
* @package Phrity > Comparison
*/
namespace Phrity\Comparison;
/**
* Interface for comparable instances.
*/
interface Comparable extends Equalable
{
/**
* Compare $this and $that and return result as comparison identifier as integer.
* @param mixed $that The instance to compare with
* @return integer Must return 0 if $this is equal to $that
* Must return -1 if $this is less than $that
* Must return 1 if $this is greater than $that
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function compare(mixed $that): int;
/**
* If $this is greater than $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is greater than $that
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function greaterThan(mixed $that): bool;
/**
* If $this is greater than or equal to $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is greater than or equal to $that
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function greaterThanOrEqual(mixed $that): bool;
/**
* If $this is less than $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is less than $that
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function lessThan(mixed $that): bool;
/**
* If $this is less than or equal to $this.
* @param mixed $that The instance to compare with
* @return boolean True if $this is less than or equal to $this
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function lessThanOrEqual(mixed $that): bool;
}
+200
View File
@@ -0,0 +1,200 @@
<?php
/**
* File for Comparator.
* @package Phrity > Comparison
*/
namespace Phrity\Comparison;
/**
* Utility class for comparing and filtering.
*/
class Comparator
{
/** @var array<Comparable> */
private $comparables;
/**
* If comparables supplied in constructor, they will be used as defaults an operations
* @param array<Comparable> $comparables List of objects implementing Comparable
*/
public function __construct(array $comparables = [])
{
$this->comparables = $comparables;
}
// Sort methods
/**
* Sorts array of comparable items, low to high
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The sorted list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function sort(array|null $comparables = null): array
{
$comparables = $comparables ?: $this->comparables;
usort($comparables, function ($item_1, $item_2) {
$this->verifyComparable($item_1);
return $item_1->compare($item_2);
});
return $comparables;
}
/**
* Sorts array of comparable items, high to low
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The sorted list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function rsort(array|null $comparables = null): array
{
$comparables = $comparables ?: $this->comparables;
usort($comparables, function ($item_1, $item_2) {
$this->verifyComparable($item_2);
return $item_2->compare($item_1);
});
return $comparables;
}
// Filter methods
/**
* Filter array of comparable items that equals condition
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function equals(Comparable $condition, array|null $comparables = null): array
{
return $this->applyFilter('equals', $condition, $comparables);
}
/**
* Filter array of comparable items that are greater than condition
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function greaterThan(Comparable $condition, array|null $comparables = null): array
{
return $this->applyFilter('greaterThan', $condition, $comparables);
}
/**
* Filter array of comparable items that are greater than or equals condition
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function greaterThanOrEqual(Comparable $condition, array|null $comparables = null): array
{
return $this->applyFilter('greaterThanOrEqual', $condition, $comparables);
}
/**
* Filter array of comparable items that are less than condition
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function lessThan(Comparable $condition, array|null $comparables = null): array
{
return $this->applyFilter('lessThan', $condition, $comparables);
}
/**
* Filter array of comparable items that are less than or equals condition
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function lessThanOrEqual(Comparable $condition, array|null $comparables = null): array
{
return $this->applyFilter('lessThanOrEqual', $condition, $comparables);
}
// Select methods
/**
* Get minimum item from array of comparable items
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return Comparable|null The resolved instance
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function min(array|null $comparables = null): Comparable|null
{
return $this->applyReduction('lessThan', $comparables);
}
/**
* Get maximum item from array of comparable items
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return Comparable|null The resolved instance
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
public function max(array|null $comparables = null): Comparable|null
{
return $this->applyReduction('greaterThan', $comparables);
}
// Private internal methods
/**
* Verify input implements Comparable
* @param mixed $item Item to verify
* @throws IncomparableException Thrown if item do not implement Comparable
*/
private function verifyComparable(mixed $item): void
{
if (!$item instanceof Comparable) {
throw new IncomparableException('All items must implement Comparable');
}
}
/**
* Filter array of comparable items according to condition instance and method
* @param string $method Comparison method to use
* @param Comparable $condition To compare against
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return array<Comparable> The filtered list
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
private function applyFilter(string $method, Comparable $condition, array|null $comparables = null): array
{
$comparables = $comparables ?: $this->comparables;
$filtered = array_filter($comparables, function ($item) use ($method, $condition) {
$this->verifyComparable($item);
return $item->$method($condition);
});
return array_values($filtered);
}
/**
* Reduce array of comparable items according comparison method
* @param string $method Comparison method to use
* @param array<Comparable>|null $comparables List of objects implementing Comparable
* @return Comparable|null The resolved instance
* @throws IncomparableException Thrown if any item in the list can not be compared
*/
private function applyReduction(string $method, array|null $comparables = null): Comparable|null
{
$comparables = $comparables ?: $this->comparables;
return array_reduce($comparables, function ($item_1, $item_2) use ($method) {
if (is_null($item_1)) {
return $item_2;
}
$this->verifyComparable($item_1);
return $item_1->$method($item_2) ? $item_1 : $item_2;
});
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* File for ComparisonTrait.
* @package Phrity > Comparison
*/
namespace Phrity\Comparison;
/**
* Trait that enables comparison methods.
*/
trait ComparisonTrait
{
/**
* If $this is equal to $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is equal to $that
* @throws IncomparableException Thrown if $this can not be compared with $that
*/
public function equals(mixed $that): bool
{
return $this->compare($that) == 0;
}
/**
* If $this is greater than $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is greater than $that
* @throws IncomparableException Thrown if $this can not be compared with $that
*/
public function greaterThan(mixed $that): bool
{
return $this->compare($that) > 0;
}
/**
* If $this is greater than or equal to $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is greater than or equal to $that
* @throws IncomparableException Thrown if $this can not be compared with $that
*/
public function greaterThanOrEqual(mixed $that): bool
{
return $this->compare($that) >= 0;
}
/**
* If $this is less than $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is less than $that
* @throws IncomparableException Thrown if $this can not be compared with $that
*/
public function lessThan(mixed $that): bool
{
return $this->compare($that) < 0;
}
/**
* If $this is less than or equal to $this.
* @param mixed $that The instance to compare with
* @return boolean True if $this is less than or equal to $this
* @throws IncomparableException Thrown if $this can not be compared with $that
*/
public function lessThanOrEqual(mixed $that): bool
{
return $this->compare($that) <= 0;
}
}
+22
View File
@@ -0,0 +1,22 @@
<?php
/**
* File for Equalable inteface.
* @package Phrity > Comparison
*/
namespace Phrity\Comparison;
/**
* Interface for equalable instances.
*/
interface Equalable
{
/**
* If $this is equal to $that.
* @param mixed $that The instance to compare with
* @return boolean True if $this is equal to $that
* @throws IncomparableException Must throw if $this can not be compared with $that
*/
public function equals(mixed $that): bool;
}
+17
View File
@@ -0,0 +1,17 @@
<?php
/**
* File for IncomparableException.
* @package Phrity > Comparison
*/
namespace Phrity\Comparison;
use InvalidArgumentException;
/**
* Exception that should be thrown when instances can not be compared.
*/
class IncomparableException extends InvalidArgumentException
{
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+40
View File
@@ -0,0 +1,40 @@
{
"name": "phrity/http",
"type": "library",
"description": "Utilities and interfaces for handling HTTP.",
"homepage": "https://phrity.sirn.se/http",
"keywords": ["HTTP", "HTTP Factories", "HTTP Serializer", "PSR-7", "PSR-17"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Http\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Http\\Test\\": "tests/"
}
},
"require": {
"php": "^8.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"guzzlehttp/psr7": "^2.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"provides": {
"psr/http-factory-implementation": "*"
}
}
+175
View File
@@ -0,0 +1,175 @@
<?php
namespace Phrity\Http;
use BadMethodCallException;
use Psr\Http\Message\{
RequestFactoryInterface,
ResponseFactoryInterface,
ServerRequestFactoryInterface,
StreamFactoryInterface,
UploadedFileFactoryInterface,
UriFactoryInterface,
RequestInterface,
ResponseInterface,
ServerRequestInterface,
StreamInterface,
UploadedFileInterface,
UriInterface,
};
/**
* Phrity\Http\HttpFactory
*/
class HttpFactory implements
RequestFactoryInterface,
ResponseFactoryInterface,
ServerRequestFactoryInterface,
StreamFactoryInterface,
UploadedFileFactoryInterface,
UriFactoryInterface
{
public function __construct(
private RequestFactoryInterface|null $requestFactory = null,
private ResponseFactoryInterface|null $responseFactory = null,
private ServerRequestFactoryInterface|null $serverRequestFactory = null,
private StreamFactoryInterface|null $streamFactory = null,
private UploadedFileFactoryInterface|null $uploadedFileFactory = null,
private UriFactoryInterface|null $uriFactory = null,
) {
}
/**
* @param string $method
* @param UriInterface|string $uri
*/
public function createRequest(string $method, mixed $uri): RequestInterface
{
if (is_null($this->requestFactory)) {
throw new BadMethodCallException('HttpFactory.createRequest not implemented.');
}
return $this->requestFactory->createRequest($method, $uri);
}
/**
* @param int $code
* @param string $reasonPhrase
*/
public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface
{
if (is_null($this->responseFactory)) {
throw new BadMethodCallException('HttpFactory.createResponse not implemented.');
}
return $this->responseFactory->createResponse($code, $reasonPhrase);
}
/**
* @param string $method
* @param UriInterface|string $uri
* @param array<string, mixed> $serverParams
*/
public function createServerRequest(string $method, mixed $uri, array $serverParams = []): ServerRequestInterface
{
if (is_null($this->serverRequestFactory)) {
throw new BadMethodCallException('HttpFactory.createServerRequest not implemented.');
}
return $this->serverRequestFactory->createServerRequest($method, $uri);
}
/**
* @param string $content
*/
public function createStream(string $content = ''): StreamInterface
{
if (is_null($this->streamFactory)) {
throw new BadMethodCallException('HttpFactory.createStream not implemented.');
}
return $this->streamFactory->createStream($content);
}
/**
* @param string $filename
* @param string $mode
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface
{
if (is_null($this->streamFactory)) {
throw new BadMethodCallException('HttpFactory.createStreamFromFile not implemented.');
}
return $this->streamFactory->createStreamFromFile($filename, $mode);
}
/**
* @param resource $resource
*/
public function createStreamFromResource($resource): StreamInterface
{
if (is_null($this->streamFactory)) {
throw new BadMethodCallException('HttpFactory.createStreamFromResource not implemented.');
}
return $this->streamFactory->createStreamFromResource($resource);
}
/**
* @param StreamInterface $stream
* @param int $size
* @param int $error
* @param string $clientFilename
* @param string $clientMediaType
*/
public function createUploadedFile(
StreamInterface $stream,
int|null $size = null,
int $error = UPLOAD_ERR_OK,
string|null $clientFilename = null,
string|null $clientMediaType = null
): UploadedFileInterface {
if (is_null($this->uploadedFileFactory)) {
throw new BadMethodCallException('HttpFactory.createUploadedFile not implemented.');
}
return $this->uploadedFileFactory->createUploadedFile(
$stream,
$size,
$error,
$clientFilename,
$clientMediaType
);
}
/**
* @param string $uri The URI to parse.
*/
public function createUri(string $uri = ''): UriInterface
{
if (is_null($this->uriFactory)) {
throw new BadMethodCallException('HttpFactory.createUri not implemented.');
}
return $this->uriFactory->createUri($uri);
}
public static function create(object ...$implementations): self
{
$created = new self();
foreach ($implementations as $implementation) {
if ($implementation instanceof RequestFactoryInterface) {
$created->requestFactory = $implementation;
}
if ($implementation instanceof ResponseFactoryInterface) {
$created->responseFactory = $implementation;
}
if ($implementation instanceof ServerRequestFactoryInterface) {
$created->serverRequestFactory = $implementation;
}
if ($implementation instanceof StreamFactoryInterface) {
$created->streamFactory = $implementation;
}
if ($implementation instanceof UploadedFileFactoryInterface) {
$created->uploadedFileFactory = $implementation;
}
if ($implementation instanceof UriFactoryInterface) {
$created->uriFactory = $implementation;
}
}
return $created;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace Phrity\Http;
use DomainException;
use Psr\Http\Message\{
MessageInterface,
ResponseInterface,
RequestInterface,
};
/**
* Phrity\Http\Serializer
*/
class Serializer
{
/**
* @param RequestInterface $request
*/
public function request(RequestInterface $request): string
{
$status = $this->wrap(
'%s %s HTTP/%s',
$request->getMethod(),
$request->getRequestTarget(),
$request->getProtocolVersion()
);
$headers = $this->formatHeaders($request);
$contents = $request->getBody()->getContents();
return sprintf("%s%s\r\n%s", $status, $headers, $contents);
}
/**
* @param ResponseInterface $response
*/
public function response(ResponseInterface $response): string
{
$status = $this->wrap(
'HTTP/%s %s %s',
$response->getProtocolVersion(),
$response->getStatusCode(),
$response->getReasonPhrase()
);
$headers = $this->formatHeaders($response);
$contents = $response->getBody()->getContents();
return sprintf("%s%s\r\n%s", $status, $headers, $contents);
}
/**
* @param MessageInterface $message
*/
public function message(MessageInterface $message): string
{
if ($message instanceof RequestInterface) {
return $this->request($message);
} elseif ($message instanceof ResponseInterface) {
return $this->response($message);
}
throw new DomainException(sprintf('Unsupported message type: %s', get_class($message)));
}
protected function formatHeaders(MessageInterface $message): string
{
$lines = '';
foreach ($message->getHeaders() as $name => $values) {
foreach ($values as $value) {
$lines .= $this->wrap('%s: %s', $name, $value);
}
}
return $lines;
}
protected function wrap(string $format, string|int ...$values): string
{
return trim(sprintf($format, ...$values)) . "\r\n";
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
{
"name": "phrity/net-stream",
"type": "library",
"description": "Socket stream classes implementing PSR-7 Stream and PSR-17 StreamFactory",
"homepage": "https://phrity.sirn.se/net-stream",
"keywords": ["socket", "stream", "stream factory", "client", "server", "PSR-7", "PSR-17"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Net\\Test\\": "tests/mock/"
}
},
"require": {
"php": "^8.1",
"phrity/util-errorhandler": "^1.1",
"phrity/util-interpolator": "^1.1",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/net-uri": "^2.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
}
}
+214
View File
@@ -0,0 +1,214 @@
<?php
namespace Phrity\Net;
use Closure;
use InvalidArgumentException;
use RuntimeException;
use Throwable;
/**
* Context class.
*/
class Context
{
/** @var resource */
private $stream;
/** @var array<int<1, 10>, Closure> */
protected array $notifiers = [];
/**
* Create exception.
* @param open-resource|null $stream
* @throws InvalidArgumentException if not a resource
* @throws InvalidArgumentException if wrong resource type
*/
public function __construct(mixed $stream = null)
{
if (is_null($stream)) {
$stream = stream_context_create();
}
$type = gettype($stream);
if ($type !== 'resource') {
throw new InvalidArgumentException("Invalid stream provided; got type '{$type}'.");
}
$rtype = get_resource_type($stream);
if (!in_array($rtype, ['stream', 'persistent stream', 'stream-context'])) {
throw new InvalidArgumentException("Invalid stream provided; got resource type '{$rtype}'.");
}
$this->stream = $stream;
stream_context_set_params($this->stream, ['notification' => function (...$input) {
$this->notifyCallback(...$input);
}]);
}
public function getOption(string $wrapper, string $option): mixed
{
return stream_context_get_options($this->stream)[$wrapper][$option] ?? null;
}
/**
* @return array<string, array<string, mixed>>
*/
public function getOptions(): array
{
return stream_context_get_options($this->stream);
}
/**
* @throws StreamException on failure
*/
public function setOption(string $wrapper, string $option, mixed $value): self
{
if (!is_resource($this->stream) || !stream_context_set_option($this->stream, $wrapper, $option, $value)) {
throw new StreamException(StreamException::CONTEXT_SET_ERR);
}
return $this;
}
/**
* @param array<string, array<string, mixed>> $options
*/
public function setOptions(array $options): self
{
foreach ($options as $wrapper => $wrapperOptions) {
foreach ($wrapperOptions as $option => $value) {
$this->setOption($wrapper, $option, $value);
}
}
return $this;
}
/**
* @deprecated Use getOption.
*/
public function getParam(string $param): mixed
{
return stream_context_get_params($this->stream)[$param] ?? null;
}
/**
* @return array<string, mixed>
* @deprecated Use getOptions.
*/
public function getParams(): array
{
return stream_context_get_params($this->stream);
}
/**
* @deprecated Use setOption and on- callbacks instead.
*/
public function setParam(string $param, mixed $value): self
{
$this->setParams([$param => $value]);
return $this;
}
/**
* @param array<string, mixed> $params
* @deprecated Use setOptions and on- callbacks instead.
* @throws StreamException on failure
*/
public function setParams(array $params): self
{
/** @phpstan-ignore booleanNot.alwaysFalse */
if (!is_resource($this->stream) || !stream_context_set_params($this->stream, $params)) {
throw new StreamException(StreamException::CONTEXT_SET_ERR);
}
return $this;
}
public function getResource(): mixed
{
return $this->stream;
}
/** @param Closure(): void $closure */
public function onResolve(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_RESOLVE] = $closure;
}
/** @param Closure(): void $closure */
public function onConnect(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_CONNECT] = $closure;
}
/** @param Closure(): void $closure */
public function onAuthRequired(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_AUTH_REQUIRED] = $closure;
}
/** @param Closure(string $mimeType): void $closure */
public function onMimeType(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_MIME_TYPE_IS] = $closure;
}
/** @param Closure(int $fileSize): void $closure */
public function onFileSize(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_FILE_SIZE_IS] = $closure;
}
/** @param Closure(string $uri): void $closure */
public function onRedirected(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_REDIRECTED] = $closure;
}
/** @param Closure(int $transferred, int $max): void $closure */
public function onProgress(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_PROGRESS] = $closure;
}
/** @param Closure(): void $closure */
public function onCompleted(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_COMPLETED] = $closure;
}
/** @param Closure(string $message, int $code): void $closure */
public function onFailure(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_FAILURE] = $closure;
}
/** @param Closure(): void $closure */
public function onAuthResult(Closure $closure): void
{
$this->notifiers[STREAM_NOTIFY_AUTH_RESULT] = $closure;
}
protected function notifyCallback(
int $code,
int $severity,
string|null $message,
int $errorCode,
int $transferred,
int $max,
): void {
if (!array_key_exists($code, $this->notifiers)) {
return;
}
$callback = $this->notifiers[$code];
$params = match ($code) {
STREAM_NOTIFY_RESOLVE,
STREAM_NOTIFY_CONNECT,
STREAM_NOTIFY_AUTH_REQUIRED,
STREAM_NOTIFY_COMPLETED,
STREAM_NOTIFY_AUTH_RESULT => [],
STREAM_NOTIFY_MIME_TYPE_IS => ['mimeType' => $message],
STREAM_NOTIFY_FILE_SIZE_IS => ['fileSize' => $message],
STREAM_NOTIFY_REDIRECTED => ['uri' => $message],
STREAM_NOTIFY_PROGRESS => ['transferred' => $transferred, 'max' => $max],
STREAM_NOTIFY_FAILURE => ['message' => $message, 'code' => $errorCode],
};
$callback(...$params);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace Phrity\Net;
use InvalidArgumentException;
use Phrity\Util\ErrorHandler;
use Psr\Http\Message\UriInterface;
/**
* Phrity\Net\SocketClient class.
*/
class SocketClient
{
protected UriInterface $uri;
protected ErrorHandler $handler;
protected bool $persistent = false;
/** @var int<0, max>|float|null */
protected int|float|null $timeout = null;
protected Context $context;
/**
* Create new socker server instance
* @param UriInterface $uri The URI to open socket on.
*/
public function __construct(UriInterface $uri, Context|null $context = null)
{
$this->uri = $uri;
$this->context = $context ?? new Context();
$this->handler = new ErrorHandler();
}
// ---------- Configuration ---------------------------------------------------------------------------------------
/**
* Set stream context.
* @param Context|array<string, array<string, mixed>>|null $options
* @param array<string, mixed>|null $params
* @return SocketClient
*/
public function setContext(Context|array|null $options = null, array|null $params = null): self
{
if ($options instanceof Context) {
$this->context = $options;
return $this;
}
// @deprecated
// @todo Add deprecation warning
$this->context->setOptions($options ?? []);
$this->context->setParams($params ?? []);
return $this;
}
public function getContext(): Context
{
return $this->context;
}
/**
* Set connection persistency.
* @param bool $persistent
* @return SocketClient
*/
public function setPersistent(bool $persistent): self
{
$this->persistent = $persistent;
return $this;
}
/**
* Set timeout in seconds.
* @param int<0, max>|float|null $timeout
* @return SocketClient
* @throws InvalidArgumentException if invalid timeout
*/
public function setTimeout(int|float|null $timeout): self
{
if (!is_null($timeout) && $timeout < 0) {
throw new InvalidArgumentException("Timeout must be 0 or more.");
}
$this->timeout = $timeout;
return $this;
}
// ---------- Operations ------------------------------------------------------------------------------------------
/**
* Create a connection on remote socket.
* @return SocketStream The stream for opened conenction.
*/
public function connect(): SocketStream
{
/** @throws StreamException if connection could not be created */
$stream = $this->handler->with(function () {
$error_code = $error_message = '';
return stream_socket_client(
$this->uri->__toString(),
$error_code,
$error_message,
$this->timeout,
$this->persistent ? STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT : STREAM_CLIENT_CONNECT,
$this->context->getResource()
);
}, new StreamException(StreamException::CLIENT_CONNECT_ERR, ['uri' => $this->uri]));
return new SocketStream($stream);
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
namespace Phrity\Net;
use ErrorException;
use InvalidArgumentException;
use Phrity\Util\ErrorHandler;
use Psr\Http\Message\UriInterface;
/**
* SocketServer class.
*/
class SocketServer extends Stream
{
/** @var array<string> */
private static array $internet_schemes = ['tcp', 'udp', 'tls', 'ssl'];
/** @var array<string> */
private static array $unix_schemes = ['unix', 'udg'];
protected ErrorHandler $handler;
protected string $address;
/** @var resource */
protected $stream;
protected Context $context;
/**
* Create new socker server instance
* @param UriInterface $uri The URI to open socket on.
* @throws StreamException if invalid scheme.
* @throws StreamException if unsupported scheme.
* @throws StreamException if unable to create socket.
*/
public function __construct(UriInterface $uri, Context|null $context = null)
{
$this->handler = new ErrorHandler();
if (!in_array($uri->getScheme(), $this->getTransports())) {
throw new StreamException(StreamException::SCHEME_TRANSPORT, ['scheme' => $uri->getScheme()]);
}
if (in_array(substr($uri->getScheme(), 0, 3), self::$internet_schemes)) {
$this->address = "{$uri->getScheme()}://{$uri->getAuthority()}";
} elseif (in_array($uri->getScheme(), self::$unix_schemes)) {
$this->address = "{$uri->getScheme()}://{$uri->getPath()}";
} else {
throw new StreamException(StreamException::SCHEME_HANDLER, ['scheme' => $uri->getScheme()]);
}
$this->context = $context ?? new Context();
/** @throws StreamException on failure */
$this->stream = $this->handler->with(function () {
$error_code = $error_message = '';
return stream_socket_server(
$this->address,
$error_code,
$error_message,
STREAM_SERVER_BIND | STREAM_SERVER_LISTEN,
$this->context->getResource()
);
}, new StreamException(StreamException::SERVER_SOCKET_ERR, ['uri' => $uri->__toString()]));
$this->evalStream();
}
// ---------- Configuration ---------------------------------------------------------------------------------------
/**
* Set stream context.
* @param Context|array<string, array<string, mixed>>|null $options
* @param array<string, mixed>|null $params
* @return static
*/
public function setContext(Context|array|null $options = null, array|null $params = null): self
{
if ($options instanceof Context) {
$this->context = $options;
return $this;
}
// @deprecated
// @todo Add deprecation warning
$this->context->setOptions($options ?? []);
$this->context->setParams($params ?? []);
return $this;
}
public function getContext(): Context
{
return $this->context;
}
/**
* Retrieve list of registered socket transports.
* @return array<string> List of registered transports.
*/
public function getTransports(): array
{
return stream_get_transports();
}
/**
* If server is in blocking mode.
* @return bool|null
*/
public function isBlocking(): bool|null
{
return $this->getMetadata('blocked');
}
/**
* Toggle blocking/non-blocking mode.
* @param bool $enable Blocking mode to set.
* @return bool If operation was succesful.
* @throws StreamException if socket is closed.
*/
public function setBlocking(bool $enable): bool
{
if (!is_resource($this->stream)) {
throw new StreamException(StreamException::SERVER_CLOSED);
}
return stream_set_blocking($this->stream, $enable);
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata(string|null $key = null): mixed
{
if (!is_resource($this->stream)) {
return null;
}
// Add URI default for version compability
$meta = array_merge([
'uri' => $this->address,
], stream_get_meta_data($this->stream));
if (isset($key)) {
return array_key_exists($key, $meta) ? $meta[$key] : null;
}
return $meta;
}
// ---------- Operations ------------------------------------------------------------------------------------------
/**
* Accept a connection on a socket.
* @param int<0, max>|float|null $timeout Override the default socket accept timeout.
* @return SocketStream|null The stream for opened conenction.
* @throws InvalidArgumentException if invalid timeout
* @throws StreamException if socket is closed
*/
public function accept(int|float|null $timeout = null): SocketStream|null
{
if (!is_null($timeout) && $timeout < 0) {
throw new InvalidArgumentException("Timeout must be 0 or more.");
}
if (!is_resource($this->stream)) {
throw new StreamException(StreamException::SERVER_CLOSED);
}
/** @throws StreamException */
$stream = $this->handler->with(function () use ($timeout) {
$peer_name = '';
return stream_socket_accept($this->stream, $timeout, $peer_name);
}, function (ErrorException $e) {
// If non-blocking mode, don't throw error on time out
if ($this->getMetadata('blocked') === false && substr_count($e->getMessage(), 'timed out') > 0) {
return null;
}
throw new StreamException(StreamException::SERVER_ACCEPT_ERR, [], $e);
});
return $stream ? new SocketStream($stream) : null;
}
}
+166
View File
@@ -0,0 +1,166 @@
<?php
namespace Phrity\Net;
use InvalidArgumentException;
/**
* SocketStream class.
*/
class SocketStream extends Stream
{
// ---------- Configuration ---------------------------------------------------------------------------------------
/**
* If stream is connected.
* @return bool
*/
public function isConnected(): bool
{
return is_resource($this->stream) && ($this->readable || $this->writable);
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): string|null
{
return is_resource($this->stream) ? (stream_socket_get_name($this->stream, true) ?: null) : null;
}
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getLocalName(): string|null
{
return is_resource($this->stream) ? (stream_socket_get_name($this->stream, false) ?: null) : null;
}
/**
* Get type of stream resoucre.
* @return string
*/
public function getResourceType(): string
{
return $this->stream ? get_resource_type($this->stream) : '';
}
/**
* If stream is in blocking mode.
* @return bool|null
*/
public function isBlocking(): bool|null
{
return $this->getMetadata('blocked');
}
/**
* Toggle blocking/non-blocking mode.
* @param bool $enable Blocking mode to set.
* @return bool If operation was succesful.
* @throws StreamException if stream is closed.
*/
public function setBlocking(bool $enable): bool
{
if (!isset($this->stream)) {
throw new StreamException(StreamException::STREAM_DETACHED);
}
return stream_set_blocking($this->stream, $enable);
}
/**
* If socket stream has unread content.
* @return bool If there is content to read.
* @throws StreamException if stream is unselectable.
*/
public function hasContents(): bool
{
if (!is_resource($this->stream)) {
return false;
}
/** @throws StreamException */
return $this->handler->with(function () {
$read = [$this->getOpenResource()];
$write = $oob = [];
return stream_select($read, $write, $oob, 0, 0) > 0;
}, new StreamException(StreamException::FAIL_SELECT));
}
/**
* Set timeout period on a stream.
* @param int<0, max>|float $timeout Seconds to be set.
* @param int|null $microseconds Microseconds to be set - deprecated
* @return bool If operation was succesful.
* @throws InvalidArgumentException if invalid timeout.
* @throws StreamException if stream is closed.
*/
public function setTimeout(int|float $timeout, int|null $microseconds = null): bool
{
// @deprecated Setting $microseconds is deprecated, use float value on $timeout instead
// @todo Add deprecation warning
if ($timeout < 0) {
throw new InvalidArgumentException("Timeout must be 0 or more.");
}
if (!isset($this->stream)) {
throw new StreamException(StreamException::STREAM_DETACHED);
}
$seconds = intval($timeout);
$microseconds = $microseconds ?? intval(round($timeout - $seconds, 6) * 1000000);
return stream_set_timeout($this->stream, $seconds, $microseconds);
}
// ---------- Operations ------------------------------------------------------------------------------------------
/**
* Read line from the stream.
* @param int<0, max> $length Read up to $length bytes from the object and return them.
* @return string|null Returns the data read from the stream, or null of eof.
* @throws StreamException if an error occurs.
*/
public function readLine(int $length): string|null
{
$stream = $this->getOpenResource();
if (!$this->readable) {
throw new StreamException(StreamException::NOT_READABLE);
}
/** @throws StreamException */
return $this->handler->with(function () use ($stream, $length) {
$result = fgets($stream, $length);
return $result === false ? null : $result;
}, new StreamException(StreamException::FAIL_GETS));
}
/**
* Closes the stream for further reading.
* @return void
*/
public function closeRead(): void
{
if (is_resource($this->stream)) {
if ($this->readable && $this->writable) {
stream_socket_shutdown($this->stream, STREAM_SHUT_RD);
$this->evalStream();
} elseif (!$this->writable) {
$this->close();
}
}
$this->readable = false;
}
/**
* Closes the stream for further writing.
* @return void
*/
public function closeWrite(): void
{
if ($this->readable && $this->writable) {
stream_socket_shutdown($this->getOpenResource(), STREAM_SHUT_WR);
$this->evalStream();
} elseif (!$this->readable) {
$this->close();
}
$this->writable = false;
}
}
+316
View File
@@ -0,0 +1,316 @@
<?php
namespace Phrity\Net;
use InvalidArgumentException;
use Phrity\Util\ErrorHandler;
use Stringable;
use Throwable;
/**
* Phrity\Net\Stream class.
* @see https://www.php-fig.org/psr/psr-7/#34-psrhttpmessagestreaminterface
*/
class Stream implements StreamInterface, Stringable
{
/** @var array<string> */
private static array $readmodes = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
/** @var array<string> */
private static array $writemodes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
/** @var resource|null */
protected $stream;
protected Context $context;
protected ErrorHandler $handler;
protected bool $readable = false;
protected bool $writable = false;
protected bool $seekable = false;
/**
* Create new stream wrapper instance
* @param resource $stream A stream resource to wrap
* @throws InvalidArgumentException If not a valid stream resource
*/
public function __construct($stream)
{
$type = gettype($stream);
if ($type !== 'resource') {
throw new InvalidArgumentException("Invalid stream provided; got type '{$type}'.");
}
$rtype = get_resource_type($stream);
if (!in_array($rtype, ['stream', 'persistent stream'])) {
throw new InvalidArgumentException("Invalid stream provided; got resource type '{$rtype}'.");
}
$this->stream = $stream;
$this->context = new Context($this->stream);
$this->handler = new ErrorHandler();
$this->evalStream();
}
// ---------- PSR-7 methods ---------------------------------------------------------------------------------------
/**
* Closes the stream and any underlying resources.
* @return void
*/
public function close(): void
{
if (is_resource($this->stream)) {
fclose($this->stream);
}
$this->stream = null;
$this->evalStream();
}
/**
* Separates any underlying resources from the stream.
* After the stream has been detached, the stream is in an unusable state.
* @return resource|null Underlying stream, if any
*/
public function detach(): mixed
{
if (!isset($this->stream)) {
return null;
}
$stream = $this->stream;
$this->stream = null;
$this->evalStream();
return $stream;
}
/**
* Get stream metadata as an associative array or retrieve a specific key.
* @param string $key Specific metadata to retrieve.
* @return array|mixed|null Returns an associative array if no key is
* provided. Returns a specific key value if a key is provided and the
* value is found, or null if the key is not found.
*/
public function getMetadata(string|null $key = null): mixed
{
if (!isset($this->stream)) {
return null;
}
$meta = stream_get_meta_data($this->stream);
if (isset($key)) {
return array_key_exists($key, $meta) ? $meta[$key] : null;
}
return $meta;
}
/**
* Returns the current position of the file read/write pointer
* @return int Position of the file pointer
* @throws StreamException on error.
*/
public function tell(): int
{
/** @throws StreamException */
return $this->handler->with(function () {
return ftell($this->getOpenResource());
}, new StreamException(StreamException::FAIL_TELL));
}
/**
* Returns true if the stream is at the end of the stream.
* @return bool
*/
public function eof(): bool
{
return empty($this->stream) || feof($this->stream);
}
/**
* Read data from the stream.
* @param int<1, max> $length Read up to $length bytes from the object and return them.
* @return string Returns the data read from the stream, or an empty string.
* @throws StreamException if an error occurs.
*/
public function read(int $length): string
{
if ($length < 1) {
throw new InvalidArgumentException("Must read minimum 1 byte");
}
$stream = $this->getOpenResource();
if (!$this->readable) {
throw new StreamException(StreamException::NOT_READABLE);
}
/** @throws StreamException */
return $this->handler->with(function () use ($stream, $length) {
return (string)fread($stream, $length);
}, new StreamException(StreamException::FAIL_READ));
}
/**
* Write data to the stream.
* @param string $string The string that is to be written.
* @return int Returns the number of bytes written to the stream.
* @throws StreamException on failure.
*/
public function write(string $string): int
{
$stream = $this->getOpenResource();
if (!$this->writable) {
throw new StreamException(StreamException::NOT_WRITABLE);
}
/** @throws StreamException */
return $this->handler->with(function () use ($stream, $string) {
return fwrite($stream, $string);
}, new StreamException(StreamException::FAIL_WRITE));
}
/**
* Get the size of the stream if known.
* @return int|null Returns the size in bytes if known, or null if unknown.
*/
public function getSize(): int|null
{
if (!is_resource($this->stream)) {
return null;
}
$stats = fstat($this->stream);
return $stats['size'] ?? null;
}
/**
* Returns whether or not the stream is seekable.
* @return bool
*/
public function isSeekable(): bool
{
return $this->seekable;
}
/**
* Seek to a position in the stream.
* @param int $offset Stream offset
* @param int $whence Specifies how the cursor position will be calculated based on the seek offset.
* @throws StreamException on failure.
*/
public function seek(int $offset, int $whence = SEEK_SET): void
{
$stream = $this->getOpenResource();
if (!$this->seekable) {
throw new StreamException(StreamException::NOT_SEEKABLE);
}
$result = fseek($stream, $offset, $whence);
if ($result !== 0) {
throw new StreamException(StreamException::FAIL_SEEK);
}
}
/**
* Seek to the beginning of the stream.
* If the stream is not seekable, this method will raise an exception;
* otherwise, it will perform a seek(0).
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* Returns whether or not the stream is writable.
* @return bool
*/
public function isWritable(): bool
{
return $this->writable;
}
/**
* Returns whether or not the stream is readable.
* @return bool
*/
public function isReadable(): bool
{
return $this->readable;
}
/**
* Returns the remaining contents in a string
* @return string
* @throws StreamException if unable to read.
* @throws StreamException if error occurs while reading.
*/
public function getContents(): string
{
$stream = $this->getOpenResource();
if (!$this->readable) {
throw new StreamException(StreamException::NOT_READABLE);
}
/** @throws StreamException */
return $this->handler->with(function () use ($stream) {
return stream_get_contents($stream);
}, new StreamException(StreamException::FAIL_CONTENTS));
}
/**
* Reads all data from the stream into a string, from the beginning to end.
* @return string
*/
public function __toString(): string
{
try {
if ($this->isSeekable()) {
$this->rewind();
}
return $this->getContents();
} catch (Throwable $e) {
trigger_error($e->getMessage(), E_USER_WARNING);
return '';
}
}
// ---------- StreamInterface methods -----------------------------------------------------------------------------
/**
* Return context for stream.
* @return Context
*/
public function getContext(): Context
{
return $this->context;
}
/**
* Return underlying resource.
* @return resource|null
*/
public function getResource(): mixed
{
return $this->stream;
}
// ---------- Protected helper methods ----------------------------------------------------------------------------
/**
* Evaluate stream state.
*/
protected function evalStream(): void
{
if ($this->stream && $meta = $this->getMetadata()) {
$mode = substr($meta['mode'], 0, 2);
$this->readable = in_array($mode, self::$readmodes);
$this->writable = in_array($mode, self::$writemodes);
$this->seekable = $meta['seekable'];
return;
}
$this->readable = $this->writable = $this->seekable = false;
}
/**
* Return underlying resource.
* @return resource
* @throws StreamException if closed.
*/
protected function getOpenResource(): mixed
{
if (!is_resource($this->stream)) {
throw new StreamException(StreamException::STREAM_DETACHED);
}
return $this->stream;
}
}
+220
View File
@@ -0,0 +1,220 @@
<?php
namespace Phrity\Net;
use Countable;
use ErrorException;
use InvalidArgumentException;
use Iterator;
use Phrity\Util\ErrorHandler;
/**
* StreamCollection class.
* @implements Iterator<string, Stream>
*/
class StreamCollection implements Countable, Iterator
{
protected ErrorHandler $handler;
/** @var array<string, StreamInterface|StreamContainerInterface> */
private array $streams = [];
/**
* Create new stream collection instance.
*/
public function __construct()
{
$this->handler = new ErrorHandler();
}
// ---------- Collectors and selectors ----------------------------------------------------------------------------
/**
* Attach stream to collection.
* @param StreamInterface|StreamContainerInterface $attach Stream to attach.
* @param string|null $key Definable name of stream.
* @return string Name of stream.
* @throws StreamException If already attached.
*/
public function attach(StreamInterface|StreamContainerInterface $attach, string|null $key = null): string
{
if ($key && array_key_exists($key, $this->streams)) {
throw new StreamException(StreamException::COLLECT_KEY_CONFLICT, ['key' => $key]);
}
$key = $key ?: $this->createKey();
$this->streams[$key] = $attach;
return $key;
}
/**
* Detach stream from collection.
* @param StreamInterface|StreamContainerInterface|string $detach Stream or name of stream to detach.
* @return bool If a stream was detached.
*/
public function detach(StreamInterface|StreamContainerInterface|string $detach): bool
{
if (is_string($detach)) {
if (array_key_exists($detach, $this->streams)) {
unset($this->streams[$detach]);
return true;
}
}
if ($detach instanceof Stream) {
foreach ($this->streams as $key => $stream) {
if ($stream === $detach) {
unset($this->streams[$key]);
return true;
}
}
}
return false;
}
/**
* Collect all readable streams into new collection.
* @return self New collection instance.
*/
public function getReadable(): self
{
$readables = new self();
foreach ($this->streams as $key => $stream) {
if ($this->getStream($stream)->isReadable()) {
$readables->attach($stream, $key);
}
}
return $readables;
}
/**
* Collect all writable streams into new collection.
* @return self New collection instance.
*/
public function getWritable(): self
{
$writables = new self();
foreach ($this->streams as $key => $stream) {
if ($this->getStream($stream)->isWritable()) {
$writables->attach($stream, $key);
}
}
return $writables;
}
/**
* Wait for redable content in stream collection.
* @param int<0, max>|float $timeout Timeout in seconds.
* @return self New collection instance.
* @throws InvalidArgumentException If invalid timeout.
*/
public function waitRead(int|float $timeout = 60): self
{
if ($timeout < 0) {
throw new InvalidArgumentException("Timeout must be 0 or more.");
}
$seconds = intval($timeout);
$microseconds = intval(round($timeout - $seconds, 6) * 1000000);
$read = [];
foreach ($this->streams as $key => $stream) {
if ($this->getStream($stream)->isReadable()) {
$read[$key] = $this->getStream($stream)->getResource();
}
}
if (empty($read)) {
return new self(); // Nothing to select
}
$changed = $this->handler->with(function () use ($read, $seconds, $microseconds) {
$write = $oob = [];
/** @phpstan-ignore argument.type */
stream_select($read, $write, $oob, $seconds, $microseconds);
return $read;
}, function (ErrorException $error) {
return []; // Ignore, but don't use result
});
$ready = new self();
foreach ($changed as $key => $resource) {
$ready->attach($this->streams[$key], $key);
}
return $ready;
}
// ---------- Countable interface implementation ------------------------------------------------------------------
/**
* Count contained streams.
* @return int Number of streams in collection.
*/
public function count(): int
{
return count($this->streams);
}
// ---------- Iterator interface implementation -------------------------------------------------------------------
/**
* Return the current stream.
* @return StreamInterface|StreamContainerInterface|null Current stream.
*/
public function current(): StreamInterface|StreamContainerInterface|null
{
return current($this->streams) ?: null;
}
/**
* Return the key of the current stream.
* @return string Current key.
*/
public function key(): string|null
{
return key($this->streams);
}
/**
* Move forward to next stream.
*/
public function next(): void
{
next($this->streams);
}
/**
* Rewind the Iterator to the first stream.
*/
public function rewind(): void
{
reset($this->streams);
}
/**
* Checks if current position is valid.
* @return bool True if valid.
*/
public function valid(): bool
{
return array_key_exists(key($this->streams) ?? -1, $this->streams);
}
// ---------- Protected helper methods ----------------------------------------------------------------------------
/**
* Create unique key.
* @return string Unique key.
*/
protected function createKey(): string
{
do {
$key = bin2hex(random_bytes(16));
} while (array_key_exists($key, $this->streams));
return $key;
}
protected function getStream(StreamInterface|StreamContainerInterface $stream): StreamInterface
{
return $stream instanceof StreamInterface ? $stream : $stream->getStream();
}
}
@@ -0,0 +1,14 @@
<?php
namespace Phrity\Net;
/**
* StreamContainerInterface.
*/
interface StreamContainerInterface
{
/**
* @return StreamInterface
*/
public function getStream(): StreamInterface;
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace Phrity\Net;
use Phrity\Util\Interpolator\InterpolatorTrait;
use RuntimeException;
use Throwable;
/**
* StreamException class.
*/
class StreamException extends RuntimeException
{
use InterpolatorTrait;
// Stream errors
public const STREAM_DETACHED = 1000;
public const NOT_READABLE = 1010;
public const NOT_WRITABLE = 1011;
public const NOT_SEEKABLE = 1012;
public const FAIL_READ = 1020;
public const FAIL_WRITE = 1021;
public const FAIL_SEEK = 1022;
public const FAIL_TELL = 1023;
public const FAIL_CONTENTS = 1024;
public const FAIL_GETS = 1025;
public const FAIL_SELECT = 1026;
// Client errors
public const CLIENT_CONNECT_ERR = 2000;
// Server errors
public const SCHEME_TRANSPORT = 3000;
public const SCHEME_HANDLER = 3001;
public const SERVER_SOCKET_ERR = 3010;
public const SERVER_CLOSED = 3011;
public const SERVER_ACCEPT_ERR = 3012;
// Collection errors
public const COLLECT_KEY_CONFLICT = 4000;
public const COLLECT_SELECT_ERR = 4001;
// Context errors
public const CONTEXT_SET_ERR = 5000;
/** @var array<int, string> */
private static array $messages = [
self::STREAM_DETACHED => 'Stream is detached.',
self::NOT_READABLE => 'Stream is not readable.',
self::NOT_WRITABLE => 'Stream is not writable.',
self::NOT_SEEKABLE => 'Stream is not seekable.',
self::FAIL_READ => 'Failed read() on stream.',
self::FAIL_WRITE => 'Failed write() on stream.',
self::FAIL_SEEK => 'Failed seek() on stream.',
self::FAIL_TELL => 'Failed tell() on stream.',
self::FAIL_CONTENTS => 'Failed getContents() on stream.',
self::FAIL_GETS => 'Failed gets() on stream.',
self::FAIL_SELECT => 'Failed select() on stream.',
self::CLIENT_CONNECT_ERR => 'Client could not connect to "{uri}".',
self::SCHEME_TRANSPORT => 'Scheme "{scheme}" is not supported.',
self::SCHEME_HANDLER => 'Could not handle scheme "{scheme}".',
self::SERVER_SOCKET_ERR => 'Could not create socket for "{uri}".',
self::SERVER_CLOSED => 'Server is closed.',
self::SERVER_ACCEPT_ERR => 'Could not accept on socket.',
self::COLLECT_KEY_CONFLICT => 'Stream with name "{key}" already attached.',
self::COLLECT_SELECT_ERR => 'Failed to select streams for reading.',
self::CONTEXT_SET_ERR => 'Failed to set option/param on context.',
];
/**
* Create exception.
* @param int $code Error code
* @param array<string, mixed> $data Additional data
* @param Throwable|null $previous Previous exception
*/
public function __construct(int $code, array $data = [], Throwable|null $previous = null)
{
$message = self::$messages[$code];
$message = $this->interpolate($message, $data);
if ($previous) {
$message .= " ({$previous->getMessage()})";
}
parent::__construct($message, $code, $previous);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?php
namespace Phrity\Net;
use InvalidArgumentException;
use Phrity\Util\ErrorHandler;
use Psr\Http\Message\{
StreamFactoryInterface,
UriInterface
};
use RuntimeException;
/**
* StreamFactory class.
* @see https://www.php-fig.org/psr/psr-17/#24-streamfactoryinterface
*/
class StreamFactory implements StreamFactoryInterface
{
/** @var array<string> */
private static array $modes = ['r', 'r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+', 'e'];
private ErrorHandler $handler;
/**
* Create new stream wrapper instance.
*/
public function __construct()
{
$this->handler = new ErrorHandler();
}
// ---------- PSR-17 methods --------------------------------------------------------------------------------------
/**
* Create a new stream from a string.
* @param string $content String content with which to populate the stream.
* @return Stream A stream instance.
*/
public function createStream(string $content = ''): Stream
{
$resource = $this->createResource('php://temp', 'r+');
fwrite($resource, $content);
return $this->createStreamFromResource($resource);
}
/**
* Create a stream from an existing file.
* @param string $filename The filename or stream URI to use as basis of stream.
* @param string $mode The mode with which to open the underlying filename/stream.
* @throws InvalidArgumentException If the mode is invalid.
* @return Stream A stream instance.
*/
public function createStreamFromFile(string $filename, string $mode = 'r'): Stream
{
if (!in_array($mode, self::$modes)) {
throw new InvalidArgumentException("Invalid mode '{$mode}'.");
}
$resource = $this->createResource($filename, $mode);
return $this->createStreamFromResource($resource);
}
/**
* Create a new stream from an existing resource.
* The stream MUST be readable and may be writable.
* @param resource $resource The PHP resource to use as the basis for the stream.
* @return Stream A stream instance.
*/
public function createStreamFromResource($resource): Stream
{
return new Stream($resource);
}
// ---------- Extensions ------------------------------------------------------------------------------------------
/**
* Create a new socket client.
* @param UriInterface $uri The URI to connect to.
* @return SocketClient A socket client instance.
*/
public function createSocketClient(UriInterface $uri, Context|null $context = null): SocketClient
{
return new SocketClient($uri, $context);
}
/**
* Create a new socket server.
* @param UriInterface $uri The URI to create server on.
* @return SocketServer A socket server instance.
*/
public function createSocketServer(UriInterface $uri, Context|null $context = null): SocketServer
{
return new SocketServer($uri, $context);
}
/**
* Create a new ocket stream from an existing resource.
* The stream MUST be readable and may be writable.
* @param resource $resource The PHP resource to use as the basis for the stream.
* @return SocketStream A socket stream instance.
*/
public function createSocketStreamFromResource($resource): SocketStream
{
return new SocketStream($resource);
}
/**
* Create a new stream collection.
* @return StreamCollection A stream collection.
*/
public function createStreamCollection(): StreamCollection
{
return new StreamCollection();
}
// ---------- Helpers ---------------------------------------------------------------------------------------------
/**
* @return resource
* @throws RuntimeException If fails to open resource
*/
private function createResource(string $filename, string $mode)
{
/** @throws RuntimeException */
return $this->handler->with(function () use ($filename, $mode) {
/** @var resource $resource */
$resource = fopen($filename, $mode);
return $resource;
}, new RuntimeException("Could not open '{$filename}'."));
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Phrity\Net;
use Psr\Http\Message\StreamInterface as PsrStreamInterface;
/**
* StreamInterface.
*/
interface StreamInterface extends PsrStreamInterface
{
/**
* @return Context
*/
public function getContext(): Context;
/**
* @return resource|null
*/
public function getResource(): mixed;
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+37
View File
@@ -0,0 +1,37 @@
{
"name": "phrity/net-uri",
"type": "library",
"description": "PSR-7 Uri and PSR-17 UriFactory implementation",
"homepage": "https://phrity.sirn.se/net-uri",
"keywords": ["uri", "uri factory", "PSR-7", "PSR-17"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Net\\": "src/"
}
},
"require": {
"php": "^8.1",
"ext-mbstring": "*",
"phrity/comparison": "^1.0",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 | ^2.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
},
"suggest": {
"ext-intl": "Enables IDN conversion for non-ASCII domains"
}
}
+710
View File
@@ -0,0 +1,710 @@
<?php
/**
* File for Net\Uri class.
* @package Phrity > Net > Uri
* @see https://www.rfc-editor.org/rfc/rfc3986
* @see https://www.php-fig.org/psr/psr-7/#35-psrhttpmessageuriinterface
*/
namespace Phrity\Net;
use InvalidArgumentException;
use JsonSerializable;
use Phrity\Comparison\{
Equalable,
IncomparableException,
};
use Psr\Http\Message\UriInterface;
use Stringable;
/**
* Net\Uri class.
*/
class Uri implements Equalable, JsonSerializable, Stringable, UriInterface
{
public const REQUIRE_PORT = 1; // Always include port, explicit or default
public const ABSOLUTE_PATH = 2; // Enforce absolute path
public const NORMALIZE_PATH = 4; // Normalize path
public const IDNA = 8; // @deprecated, replaced by IDN_ENCODE
public const IDN_ENCODE = 16; // IDN-encode host
public const IDN_DECODE = 32; // IDN-decode host
public const URI_DECODE = 64; // Decoded URI
public const URI_ENCODE = 128; // Minimal URI encoded
public const URI_ENCODE_3986 = 256; // URI encoded RFC 3986
private const RE_MAIN = '!^(?P<schemec>(?P<scheme>[^:/?#]+):)?(?P<authorityc>//(?P<authority>[^/?#]*))?'
. '(?P<path>[^?#]*)(?P<queryc>\?(?P<query>[^#]*))?(?P<fragmentc>#(?P<fragment>.*))?$!';
private const RE_AUTH = '!^(?P<userinfoc>(?P<user>[^:/?#]+)(?P<passc>:(?P<pass>[^:/?#]+))?@)?'
. '(?P<host>[^:/?#]*|\[[^/?#]*\])(?P<portc>:(?P<port>[0-9]*))?$!';
/** @var array<string, int> $portDefaults */
private static array $portDefaults = [
'acap' => 674,
'afp' => 548,
'dict' => 2628,
'dns' => 53,
'ftp' => 21,
'git' => 9418,
'gopher' => 70,
'http' => 80,
'https' => 443,
'imap' => 143,
'ipp' => 631,
'ipps' => 631,
'irc' => 194,
'ircs' => 6697,
'ldap' => 389,
'ldaps' => 636,
'mms' => 1755,
'msrp' => 2855,
'mtqp' => 1038,
'nfs' => 111,
'nntp' => 119,
'nntps' => 563,
'pop' => 110,
'prospero' => 1525,
'redis' => 6379,
'rsync' => 873,
'rtsp' => 554,
'rtsps' => 322,
'rtspu' => 5005,
'sftp' => 22,
'smb' => 445,
'snmp' => 161,
'ssh' => 22,
'svn' => 3690,
'telnet' => 23,
'ventrilo' => 3784,
'vnc' => 5900,
'wais' => 210,
'ws' => 80,
'wss' => 443,
];
private string $scheme = '';
private bool $authority = false;
private string $host = '';
private int|null $port = null;
private string $user = '';
private string|null $pass = null;
private string $path = '';
private string $query = '';
private string $fragment = '';
/**
* Create new URI instance using a string
* @param string $uriString URI as string
* @throws InvalidArgumentException If the given URI cannot be parsed
*/
public function __construct(string $uriString = '')
{
$this->parse($uriString);
}
// ---------- PSR-7 getters ---------------------------------------------------------------------------------------
/**
* Retrieve the scheme component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI scheme
*/
public function getScheme(int $flags = 0): string
{
return $this->scheme;
}
/**
* Retrieve the authority component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI authority, in "[user-info@]host[:port]" format
*/
public function getAuthority(int $flags = 0): string
{
$host = $this->formatComponent($this->getHost($flags));
if ($host === '') {
return '';
}
$userinfo = $this->formatComponent($this->getUserInfo($flags), '', '@');
$port = $this->formatComponent($this->getPort($flags), ':');
return "{$userinfo}{$host}{$port}";
}
/**
* Retrieve the user information component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI user information, in "username[:password]" format
*/
public function getUserInfo(int $flags = 0): string
{
$user = $this->formatComponent($this->uriEncode($this->user, $flags));
$pass = $this->formatComponent($this->uriEncode($this->pass ?? '', $flags), ':');
return $user === '' ? '' : "{$user}{$pass}";
}
/**
* Retrieve the host component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI host
*/
public function getHost(int $flags = 0): string
{
if ($flags & self::IDNA) {
trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED);
return $this->idnEncode($this->host);
}
if ($flags & self::IDN_ENCODE) {
return $this->idnEncode($this->host);
}
if ($flags & self::IDN_DECODE) {
return $this->idnDecode($this->host);
}
return $this->host;
}
/**
* Retrieve the port component of the URI.
* @param int $flags Optional modifier flags
* @return null|int The URI port
*/
public function getPort(int $flags = 0): int|null
{
$default = self::$portDefaults[$this->scheme] ?? null;
if ($flags & self::REQUIRE_PORT) {
return $this->port !== null ? $this->port : $default;
}
return $this->port === $default ? null : $this->port;
}
/**
* Retrieve the path component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI path
*/
public function getPath(int $flags = 0): string
{
$path = $this->path;
if ($flags & self::NORMALIZE_PATH) {
$path = $this->normalizePath($path);
}
if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
$path = "/{$path}";
}
return $this->uriEncode($path, $flags, '\/:@');
}
/**
* Retrieve the query string of the URI.
* @param int $flags Optional modifier flags
* @return string The URI query string
*/
public function getQuery(int $flags = 0): string
{
return $this->uriEncode($this->query, $flags, '\/:@?');
}
/**
* Retrieve the fragment component of the URI.
* @param int $flags Optional modifier flags
* @return string The URI fragment
*/
public function getFragment(int $flags = 0): string
{
return $this->uriEncode($this->fragment, $flags, '\/:@?');
}
// ---------- PSR-7 setters ---------------------------------------------------------------------------------------
/**
* Return an instance with the specified scheme.
* @param string $scheme The scheme to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified scheme
* @throws InvalidArgumentException for invalid schemes
* @throws InvalidArgumentException for unsupported schemes
*/
public function withScheme(string $scheme, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setScheme($scheme, $flags);
return $clone;
}
/**
* Return an instance with the specified user information.
* @param string $user The user name to use for authority
* @param null|string $password The password associated with $user
* @param int $flags Optional modifier flags
* @return self A new instance with the specified user information
*/
public function withUserInfo(string $user, string|null $password = null, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setUserInfo($user, $password);
return $clone;
}
/**
* Return an instance with the specified host.
* @param string $host The hostname to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified host
* @throws InvalidArgumentException for invalid hostnames
*/
public function withHost(string $host, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setHost($host, $flags);
return $clone;
}
/**
* Return an instance with the specified port.
* @param null|int $port The port to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified port
* @throws InvalidArgumentException for invalid ports
*/
public function withPort(int|null $port, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setPort($port, $flags);
return $clone;
}
/**
* Return an instance with the specified path.
* @param string $path The path to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified path
* @throws InvalidArgumentException for invalid paths
*/
public function withPath(string $path, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setPath($path, $flags);
return $clone;
}
/**
* Return an instance with the specified query string.
* @param string $query The query string to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified query string
* @throws InvalidArgumentException for invalid query strings
*/
public function withQuery(string $query, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setQuery($query, $flags);
return $clone;
}
/**
* Return an instance with the specified URI fragment.
* @param string $fragment The fragment to use with the new instance
* @param int $flags Optional modifier flags
* @return self A new instance with the specified fragment
*/
public function withFragment(string $fragment, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setFragment($fragment, $flags);
return $clone;
}
// ---------- PSR-7 string & Stringable ---------------------------------------------------------------------------
/**
* Return the string representation as a URI reference.
* @return string
*/
public function __toString(): string
{
return $this->toString();
}
// ---------- JsonSerializable ------------------------------------------------------------------------------------
/**
* Return JSON encode value as URI reference.
* @return string
*/
public function jsonSerialize(): string
{
return $this->toString();
}
// ---------- Equalable ------------------------------------------------------------------------------------------
/**
* Return JSON encode value as URI reference.
* @param UriInterface|string $compareWith
* @return bool
*/
public function equals(mixed $compareWith): bool
{
if (!$compareWith instanceof UriInterface && !is_string($compareWith)) {
throw new IncomparableException(sprintf("Can not compare with type '%s'", get_debug_type($compareWith)));
}
$flags = self::REQUIRE_PORT | self::NORMALIZE_PATH | self::IDN_ENCODE;
$them = $compareWith instanceof self ? $compareWith : new self((string)$compareWith);
return $this->toString($flags) == $them->toString($flags);
}
// ---------- Extensions ------------------------------------------------------------------------------------------
/**
* Return the string representation as a URI reference.
* @param int $flags Optional modifier flags
* @param string $format Optional format specification
* @return string
*/
public function toString(int $flags = 0, string $format = '{scheme}{authority}{path}{query}{fragment}'): string
{
$pathFlags = ($this->authority && $this->path ? self::ABSOLUTE_PATH : 0) | $flags;
return str_replace([
'{scheme}',
'{authority}',
'{path}',
'{query}',
'{fragment}',
], [
$this->formatComponent($this->getScheme($flags), '', ':'),
$this->authority ? "//{$this->formatComponent($this->getAuthority($flags))}" : '',
$this->formatComponent($this->getPath($pathFlags)),
$this->formatComponent($this->getQuery(), '?'),
$this->formatComponent($this->getFragment(), '#'),
], $format);
}
/**
* Get compontsns as array; as parse_url() method
* @param int $flags Optional modifier flags
* @return array<string, mixed>
*/
public function getComponents(int $flags = 0): array
{
return array_filter([
'scheme' => $this->getScheme($flags),
'host' => $this->getHost($flags),
'port' => $this->getPort($flags | self::REQUIRE_PORT),
'user' => $this->user,
'pass' => $this->pass,
'path' => $this->getPath($flags),
'query' => $this->getQuery($flags),
'fragment' => $this->getFragment($flags),
]);
}
/**
* Return an instance with the specified compontents set.
* @param array<string, mixed> $components
* @param int<0, 256> $flags
* @return self A new instance with the specified components
*/
public function withComponents(array $components, int $flags = 0): self
{
$clone = $this->clone($flags);
foreach ($components as $component => $value) {
switch ($component) {
case 'port':
$clone->setPort($value, $flags);
break;
case 'scheme':
$clone->setScheme($value, $flags);
break;
case 'host':
$clone->setHost($value, $flags);
break;
case 'path':
$clone->setPath($value, $flags);
break;
case 'query':
$clone->setQuery($value, $flags);
break;
case 'fragment':
$clone->setFragment($value, $flags);
break;
case 'userInfo':
$clone->setUserInfo(...$value);
break;
default:
throw new InvalidArgumentException("Invalid URI component: '{$component}'");
}
}
return $clone;
}
/**
* Return all query items (if any) as associative array.
* @param int $flags Optional modifier flags
* @return array<array-key, mixed> Query items
*/
public function getQueryItems(int $flags = 0): array
{
parse_str($this->getQuery(), $result);
return $result;
}
/**
* Return query item value for named query item, or null if not present.
* @param string $name Name of query item to retrieve
* @param int $flags Optional modifier flags
* @return string|null|array<string, mixed> Query item value
*/
public function getQueryItem(string $name, int $flags = 0): array|string|null
{
parse_str($this->getQuery(), $result);
return $result[$name] ?? null;
}
/**
* Add query items as associative array that will be merged qith current items.
* @param array<string, mixed> $items Array of query items to add
* @param int $flags Optional modifier flags
* @return self A new instance with the added query items
*/
public function withQueryItems(array $items, int $flags = 0): self
{
$clone = $this->clone($flags);
$clone->setQuery(http_build_query(
$this->queryMerge($this->getQueryItems($flags), $items),
'',
null,
PHP_QUERY_RFC3986
), $flags);
return $clone;
}
/**
* Add query item value for named query item
* @param string $name Name of query item to add
* @param string|null|array<string, mixed> $value Value of query item to add
* @param int $flags Optional modifier flags
* @return self A new instance with the added query items
*/
public function withQueryItem(string $name, array|string|null $value, int $flags = 0): self
{
return $this->withQueryItems([$name => $value], $flags);
}
// ---------- Protected helper methods ----------------------------------------------------------------------------
protected function setPort(int|null $port, int $flags = 0): void
{
if ($port !== null && ($port < 0 || $port > 65535)) {
throw new InvalidArgumentException("Invalid port '{$port}'");
}
$this->port = $port;
}
protected function setScheme(string $scheme, int $flags = 0): void
{
$pattern = '/^[a-z][a-z0-9-+.]*$/i';
if ($scheme !== '' && preg_match($pattern, $scheme) == 0) {
throw new InvalidArgumentException("Invalid scheme '{$scheme}': Should match {$pattern}");
}
$this->scheme = mb_strtolower($scheme);
}
protected function setHost(string $host, int $flags = 0): void
{
$this->authority = $this->authority || $host !== '';
if ($flags & self::IDNA) {
trigger_error("Flag IDNA is deprecated; use IDN_ENCODE instead", E_USER_DEPRECATED);
$host = $this->idnEncode($host);
}
if ($flags & self::IDN_ENCODE) {
$host = $this->idnEncode($host);
}
if ($flags & self::IDN_DECODE) {
$host = $this->idnDecode($host);
}
$this->host = mb_strtolower($host);
}
protected function setPath(string $path, int $flags = 0): void
{
if ($flags & self::NORMALIZE_PATH) {
$path = $this->normalizePath($path);
}
if ($flags & self::ABSOLUTE_PATH && substr($path, 0, 1) !== '/') {
$path = "/{$path}";
}
$this->path = $this->uriDecode($path);
}
protected function setQuery(string $query, int $flags = 0): void
{
$this->query = $this->uriDecode($query);
}
protected function setFragment(string $fragment, int $flags = 0): void
{
$this->fragment = $this->uriDecode($fragment);
}
protected function setUser(string $user, int $flags = 0): void
{
$this->user = $this->uriDecode($user);
}
protected function setPassword(string|null $pass, int $flags = 0): void
{
$this->pass = $pass === null ? null : $this->uriDecode($pass);
}
protected function setUserInfo(string $user = '', string|null $pass = null, int $flags = 0): void
{
$this->setUser($user);
$this->setPassword($pass);
}
// ---------- Private helper methods ------------------------------------------------------------------------------
private function parse(string $uriString = ''): void
{
if ($uriString === '') {
return;
}
preg_match(self::RE_MAIN, $uriString, $main);
$this->authority = !empty($main['authorityc']);
$this->setScheme($main['scheme'] ?? '');
$this->setPath($main['path'] ?? '');
$this->setQuery($main['query'] ?? '');
$this->setFragment($main['fragment'] ?? '');
if ($this->authority && !empty($main['authority'])) {
preg_match(self::RE_AUTH, $main['authority'], $auth);
if (empty($auth)) {
throw new InvalidArgumentException("Invalid 'authority'.");
}
if ($auth['host'] === '' && $auth['user'] !== '') {
throw new InvalidArgumentException("Invalid 'authority'.");
}
$this->setUser($auth['user'] ?? '');
$this->setPassword($auth['pass'] ?? null);
$this->setHost($auth['host'] ?? '');
$this->setPort(isset($auth['port']) ? (int)$auth['port'] : null);
}
}
private function clone(int $flags = 0): self
{
$clone = clone $this;
if ($flags & self::REQUIRE_PORT) {
$clone->setPort($this->getPort(self::REQUIRE_PORT), $flags);
}
return $clone;
}
private function uriEncode(string $source, int $flags = 0, string $keep = ''): string
{
if ($flags & self::URI_DECODE) {
return $source;
}
$unreserved = 'a-zA-Z0-9_\-\.~';
$subdelim = '!\$&\'\(\)\*\+,;=';
$char = '\pL';
$pct = '%(?![A-Fa-f0-9]{2}))';
$re = "/(?:[^%{$unreserved}{$subdelim}{$keep}]+|{$pct}/u";
if ($flags & self::URI_ENCODE) {
$re = "/(?:[^%{$unreserved}{$subdelim}{$keep}{$char}]+|{$pct}/u";
}
return preg_replace_callback($re, function ($matches) {
return rawurlencode($matches[0]);
}, $source) ?? $source;
}
private function uriDecode(string $source): string
{
$re = "/(%[A-Fa-f0-9]{2})/u";
return preg_replace_callback($re, function ($matches) {
return rawurldecode($matches[0]);
}, $source) ?? $source;
}
private function formatComponent(string|int|null $value, string $before = '', string $after = ''): string
{
$string = strval($value);
return $string === '' ? '' : "{$before}{$string}{$after}";
}
private function normalizePath(string $path): string
{
$result = [];
preg_match_all('!([^/]*/|[^/]*$)!', $path, $items);
foreach ($items[0] as $item) {
switch ($item) {
case '':
case './':
case '.':
break; // just skip
case '/':
if (empty($result)) {
array_push($result, $item); // add
}
break;
case '..':
case '../':
if (empty($result) || end($result) == '../') {
array_push($result, $item); // add
} else {
array_pop($result); // remove previous
}
break;
default:
array_push($result, $item); // add
}
}
return implode('', $result);
}
private function idnEncode(string $value): string
{
if ($value === '' || !function_exists('idn_to_ascii')) {
return $value; // Can't convert, but don't cause exception
}
return idn_to_ascii($value, IDNA_NONTRANSITIONAL_TO_ASCII, INTL_IDNA_VARIANT_UTS46) ?: $value;
}
private function idnDecode(string $value): string
{
if ($value === '' || !function_exists('idn_to_utf8')) {
return $value; // Can't convert, but don't cause exception
}
return idn_to_utf8($value, IDNA_NONTRANSITIONAL_TO_UNICODE, INTL_IDNA_VARIANT_UTS46) ?: $value;
}
/**
* @param array<string, mixed> $a
* @param array<string, mixed> $b
* @return array<string, mixed>
*/
private function queryMerge(array $a, array $b): array
{
foreach ($b as $key => $value) {
if (is_int($key)) {
$a[] = $value;
} elseif (is_array($value)) {
$a[$key] = $this->queryMerge($a[$key] ?? [], $b[$key] ?? []);
} elseif (is_scalar($value)) {
$a[$key] = $this->uriDecode($b[$key]);
} else {
unset($a[$key]);
}
}
return $a;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
/**
* File for Net\UriFactory class.
* @package Phrity > Net > Uri
* @see https://www.rfc-editor.org/rfc/rfc3986
* @see https://www.php-fig.org/psr/psr-17/#26-urifactoryinterface
*/
namespace Phrity\Net;
use InvalidArgumentException;
use Psr\Http\Message\{
UriFactoryInterface,
UriInterface
};
/**
* Net\UriFactory class.
*/
class UriFactory implements UriFactoryInterface
{
// ---------- PSR-7 methods ---------------------------------------------------------------------------------------
/**
* Create a new URI.
* @param string $uri The URI to parse.
* @throws InvalidArgumentException If the given URI cannot be parsed
*/
public function createUri(string $uri = ''): UriInterface
{
return new Uri($uri);
}
// ---------- Extensions ------------------------------------------------------------------------------------------
/**
* Create a new URI from existing.
* @param UriInterface $uri A URI instance to create from.
* @throws InvalidArgumentException If the given URI cannot be parsed
*/
public function createUriFromInterface(UriInterface $uri): UriInterface
{
return new Uri($uri->__toString());
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+35
View File
@@ -0,0 +1,35 @@
{
"name": "phrity/util-accessor",
"type": "library",
"description": "Utility to handle access to a data set by using access paths. Similar to and partly compatible with xpath and json-pointer.",
"homepage": "https://phrity.sirn.se/util-accessor",
"keywords": ["accessor"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Util\\Test\\": "tests/"
}
},
"require": {
"php": "^8.1",
"phrity/util-transformer": "^1.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
}
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace Phrity\Util;
use Phrity\Util\Transformer\TransformerInterface;
/**
* Accessor utility class.
*/
class Accessor
{
use AccessorTrait;
/**
* @var non-empty-string $separator Separator
*/
protected string $separator;
/**
* Constructor for this class.
* @param non-empty-string $separator Separator
* @param TransformerInterface|null $transformer Transformer
*/
public function __construct(string $separator = '/', TransformerInterface|null $transformer = null)
{
$this->separator = $separator;
$this->accessorTransformer = $transformer;
}
/**
* Get specified content from data set.
* @param mixed $data Data set to access
* @param string $path Path to access
* @param mixed $default Default value
* @param string|null $coerce Optional type coercion
* @return mixed Specified content of data set
*/
public function get(mixed $data, string $path, mixed $default = null, string|null $coerce = null): mixed
{
return $this->accessorGet($data, $this->accessorParsePath($path, $this->separator), $default, $coerce);
}
/**
* Check specified content in data set.
* @param mixed $data Data set to access
* @param string $path Path to access
* @return bool If speciefied content is present
*/
public function has(mixed $data, string $path): bool
{
return $this->accessorHas($data, $this->accessorParsePath($path, $this->separator));
}
/**
* Set specified content on data set.
* @param mixed $data Data set to modify
* @param string $path Path to access
* @param mixed $value Value to set
* @return mixed Modified data set
*/
public function set(mixed $data, string $path, mixed $value): mixed
{
return $this->accessorSet($data, $this->accessorParsePath($path, $this->separator), $value);
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace Phrity\Util;
use RuntimeException;
/**
* Accessor exception class.
*/
class AccessorException extends RuntimeException
{
}
+124
View File
@@ -0,0 +1,124 @@
<?php
namespace Phrity\Util;
use Phrity\Util\Transformer\{
BasicTypeConverter,
TransformerInterface,
Type,
};
/**
* Accessor utility trait.
*/
trait AccessorTrait
{
private TransformerInterface|null $accessorTransformer = null;
/**
* Recursive worker function for get() operation.
* @param mixed $data Data set to access
* @param array<string> $path Path to access
* @param mixed $default Default value
* @param string|null $coerce Optional type coercion
* @return mixed Specified content of data set
*/
private function accessorGet(mixed $data, array $path, mixed $default, string|null $coerce = null): mixed
{
if (empty($path)) {
if ($coerce && $this->accessorGetTransformer()->canTransform($data, $coerce)) {
return $this->accessorGetTransformer()->transform($data, $coerce);
}
return $data; // Bottom case
}
$current = array_shift($path);
if ($this->accessorGetTransformer()->canTransform($data)) {
$data = $this->accessorGetTransformer()->transform($data);
}
if (is_array($data) && array_key_exists($current, $data)) {
return $this->accessorGet($data[$current], $path, $default, $coerce);
}
if (is_object($data) && property_exists($data, $current)) {
return $this->accessorGet($data->$current, $path, $default, $coerce);
}
return $default; // No match
}
/**
* Recursive worker function for has() operation.
* @param mixed $data Data set to access
* @param array<string> $path Path to access
* @return bool If speciefied content is present
*/
private function accessorHas(mixed $data, array $path): bool
{
if (empty($path)) {
return true; // Bottom case
}
$current = array_shift($path);
if ($this->accessorGetTransformer()->canTransform($data)) {
$data = $this->accessorGetTransformer()->transform($data);
}
if (is_array($data) && array_key_exists($current, $data)) {
return $this->accessorHas($data[$current], $path);
}
if (is_object($data) && property_exists($data, $current)) {
return $this->accessorHas($data->$current, $path);
}
return false; // No match
}
/**
* Recursive worker function for set() operation.
* @param mixed $data Data set to modify
* @param array<string> $path Path to access
* @param mixed $value Value to set
* @return mixed Modified data set
*/
private function accessorSet(mixed $data, array $path, mixed $value): mixed
{
if (empty($path)) {
return $value; // Bottom case
}
$current = array_shift($path);
if ($this->accessorGetTransformer()->canTransform($data)) {
$data = $this->accessorGetTransformer()->transform($data);
}
if (is_object($data)) {
$data->$current = $this->accessorSet($data->$current ?? null, $path, $value);
}
if (is_array($data)) {
$data[$current] = $this->accessorSet($data[$current] ?? null, $path, $value);
}
if (is_null($data) || is_scalar($data)) {
$data = [];
$data[$current] = $this->accessorSet(null, $path, $value);
}
return $data;
}
/**
* Parse string path into array segments.
* @param string $path Path to parse
* @param non-empty-string $separator Separator token
* @return array<string> Path segments as array
*/
private function accessorParsePath(string $path, string $separator): array
{
return array_filter(explode($separator, $path), function (string $item): bool {
return $item !== '';
});
}
/**
* Get or set Transformer to use, BasicTypeConverter used as default.
* @return TransformerInterface
*/
private function accessorGetTransformer(): TransformerInterface
{
if (!$this->accessorTransformer) {
$this->accessorTransformer = new BasicTypeConverter(); // Default type converter
}
return $this->accessorTransformer;
}
}
+76
View File
@@ -0,0 +1,76 @@
<?php
namespace Phrity\Util;
use JsonSerializable;
use Phrity\Util\Transformer\TransformerInterface;
/**
* DataAccessor utility class.
*/
class DataAccessor implements JsonSerializable
{
use AccessorTrait;
/**
* @var mixed $data Data set
*/
protected mixed $data;
/**
* @var non-empty-string $separator Separator
*/
protected string $separator;
/**
* Constructor for this class.
* @param mixed $data Data set to access
* @param non-empty-string $separator Separator
* @param TransformerInterface|null $transformer Transformer
*/
public function __construct(mixed $data, string $separator = '/', TransformerInterface|null $transformer = null)
{
$this->data = $data;
$this->separator = $separator;
$this->accessorTransformer = $transformer;
}
/**
* Get specified content from data set.
* @param string $path Path to access
* @param mixed $default Default value
* @param string|null $coerce Optional type coercion
* @return mixed Specified content of data set
*/
public function get(string $path, mixed $default = null, string|null $coerce = null): mixed
{
return $this->accessorGet($this->data, $this->accessorParsePath($path, $this->separator), $default, $coerce);
}
/**
* Check specified content in data set.
* @param string $path Path to access
* @return bool If speciefied content is present
*/
public function has(string $path): bool
{
return $this->accessorHas($this->data, $this->accessorParsePath($path, $this->separator));
}
/**
* Set specified content on data set.
* @param string $path Path to access
* @param mixed $value Value to set
* @return mixed Modified data set
*/
public function set(string $path, mixed $value): mixed
{
$this->data = $this->accessorSet($this->data, $this->accessorParsePath($path, $this->separator), $value);
return $this->data;
}
public function jsonSerialize(): mixed
{
return $this->data;
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace Phrity\Util;
use Phrity\Util\Transformer\TransformerInterface;
/**
* PathAccessor utility class.
*/
class PathAccessor
{
use AccessorTrait;
/**
* @var string $path Path
*/
protected string $path;
/**
* @var non-empty-string $separator Separator
*/
protected string $separator;
/**
* Constructor for this class.
* @param string $path Path
* @param non-empty-string $separator Separator
* @param TransformerInterface|null $transformer Transformer
*/
public function __construct(string $path, string $separator = '/', TransformerInterface|null $transformer = null)
{
$this->path = $path;
$this->separator = $separator;
$this->accessorTransformer = $transformer;
}
/**
* Get specified content from data set.
* @param mixed $data Data set to access
* @param mixed $default Default value
* @return mixed Specified content of data set
*/
public function get(mixed $data, mixed $default = null, string|null $coerce = null): mixed
{
return $this->accessorGet($data, $this->accessorParsePath($this->path, $this->separator), $default, $coerce);
}
/**
* Check specified content in data set.
* @param mixed $data Data set to access
* @return bool If speciefied content is present
*/
public function has(mixed $data): bool
{
return $this->accessorHas($data, $this->accessorParsePath($this->path, $this->separator));
}
/**
* Set specified content on data set.
* @param mixed $data Data set to modify
* @param mixed $value Value to set
* @return mixed Modified data set
*/
public function set(mixed $data, mixed $value): mixed
{
return $this->accessorSet($data, $this->accessorParsePath($this->path, $this->separator), $value);
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+34
View File
@@ -0,0 +1,34 @@
{
"name": "phrity/util-errorhandler",
"type": "library",
"description": "Inline error handler; catch and resolve errors for code block.",
"homepage": "https://phrity.sirn.se/util-errorhandler",
"keywords": ["error", "warning"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Util\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Util\\Tests\\": "tests/"
}
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
}
}
+131
View File
@@ -0,0 +1,131 @@
<?php
/**
* File for ErrorHandler utility class.
* @package Phrity > Util > ErrorHandler
*/
namespace Phrity\Util;
use Closure;
use ErrorException;
use Throwable;
/**
* ErrorHandler utility class.
* Allows catching and resolving errors inline.
*/
class ErrorHandler
{
/* ----------------- Public methods ---------------------------------------------- */
/**
* Set error handler to run until removed.
* @param Closure|Throwable|null $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return (callable(): mixed)|null Previously registered error handler, if any
*/
public function set(Closure|Throwable|null $handling = null, int $levels = E_ALL): callable|null
{
return set_error_handler($this->getHandler($handling), $levels);
}
/**
* Remove error handler.
* @return bool True if removed
*/
public function restore(): bool
{
return restore_error_handler();
}
/**
* Run code with error handling, breaks on first encountered error.
* @param callable $callback The code to run
* @param Closure|Throwable|null $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return mixed Return what $callback returns, or what $handling retuns on error
*/
public function with(callable $callback, Closure|Throwable|null $handling = null, int $levels = E_ALL): mixed
{
$error = null;
$result = null;
try {
$this->set(null, $levels);
$result = $callback();
} catch (ErrorException $e) {
$error = $this->handle($handling, $e);
} finally {
$this->restore();
}
return $error ?? $result;
}
/**
* Run code with error handling, comletes code before handling errors
* @param callable $callback The code to run
* @param Closure|Throwable|null $handling
* - If null, handler will throw ErrorException
* - If Throwable $t, throw $t with ErrorException attached as previous
* - If callable, will invoke callback with ErrorException as argument
* @param int $levels Error levels to catch, all errors by default
* @return mixed Return what $callback returns, or what $handling retuns on error
*/
public function withAll(callable $callback, Closure|Throwable|null $handling = null, int $levels = E_ALL): mixed
{
$errors = [];
$this->set(function (ErrorException $e) use (&$errors) {
$errors[] = $e;
}, $levels);
$result = $callback();
$this->restore();
$error = empty($errors) ? null : $this->handle($handling, $errors, $result);
return $error ?? $result;
}
/* ----------------- Private helpers --------------------------------------------- */
// Get handler function
private function getHandler(Closure|Throwable|null $handling): Closure
{
return function ($severity, $message, $file, $line) use ($handling) {
$error = new ErrorException($message, 0, $severity, $file, $line);
$this->handle($handling, $error);
};
}
/**
* Handle error according to $handlig type
* @param Closure|Throwable|null $handling
* @param ErrorException|non-empty-array<ErrorException> $error
* @mixed $result
* @return mixed
* @throws Throwable
*/
private function handle(Closure|Throwable|null $handling, ErrorException|array $error, mixed $result = null): mixed
{
if (is_callable($handling)) {
return $handling($error, $result);
}
if (is_array($error)) {
$error = array_shift($error);
}
if ($handling instanceof Throwable) {
try {
/* @phpstan-ignore finally.exitPoint */
throw $error;
} finally {
/* @phpstan-ignore finally.exitPoint */
throw $handling;
}
}
throw $error;
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+36
View File
@@ -0,0 +1,36 @@
{
"name": "phrity/util-interpolator",
"type": "library",
"description": "Interpolator class and trait.",
"homepage": "https://phrity.sirn.se/util-interpolator",
"keywords": ["interpolator", "interpolate"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Util\\Interpolator\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Util\\Interpolator\\Test\\": "tests/"
}
},
"require": {
"php": "^8.1",
"phrity/util-accessor": "^1.3",
"phrity/util-transformer": "^1.3"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0"
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace Phrity\Util\Interpolator;
use Phrity\Util\Transformer\TransformerInterface;
use Stringable;
class Interpolator
{
use InterpolatorTrait {
interpolate as protected traitInterpolate;
}
/**
* @param non-empty-string $separator
* @param TransformerInterface|null $transformer
*/
public function __construct(
private string $separator = '.',
private TransformerInterface|null $transformer = null,
) {
}
/**
* @param string|Stringable $source
* @param array<array-key, mixed>|object $replacers
*/
public function interpolate(
string|Stringable $source,
array|object $replacers = [],
): string {
return $this->traitInterpolate($source, $replacers, $this->separator, $this->transformer);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Phrity\Util\Interpolator;
use Phrity\Util\DataAccessor;
use Phrity\Util\Transformer\{
BasicTypeConverter,
DateTimeConverter,
EnumConverter,
FirstMatchResolver,
ReadableConverter,
StringableConverter,
ThrowableConverter,
TransformerInterface,
Type,
};
use Stringable;
trait InterpolatorTrait
{
/**
* @param string|Stringable $source
* @param array<array-key, mixed>|object $replacers
* @param non-empty-string $separator
* @param TransformerInterface|null $transformer
*/
public function interpolate(
string|Stringable $source,
array|object $replacers = [],
string $separator = '.',
TransformerInterface|null $transformer = null,
): string {
$transformer = $transformer ?? new FirstMatchResolver([
new DateTimeConverter(),
new EnumConverter(),
new ReadableConverter(),
new ThrowableConverter(),
new StringableConverter(),
new BasicTypeConverter(),
]);
$accessor = new DataAccessor($replacers, $separator, $transformer);
$result = preg_replace_callback('/{([^{}]{1,})}/', function (array $matches) use ($accessor): string {
return $accessor->get($matches[1], $matches[0], Type::STRING);
}, $source);
return $result ?? $source;
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Sören Jensen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+42
View File
@@ -0,0 +1,42 @@
{
"name": "phrity/util-transformer",
"type": "library",
"description": "Type transformers, normalizers and resolvers.",
"homepage": "https://phrity.sirn.se/util-transformer",
"keywords": ["type", "transformer", "normalizer", "resolver"],
"license": "MIT",
"authors": [
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"Phrity\\Util\\Transformer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Phrity\\Util\\Transformer\\Test\\": "tests/"
}
},
"require": {
"php": "^8.1"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^3.5 || ^4.0",
"symfony/property-access": "^6.0 || ^7.0 || ^8.0",
"symfony/serializer": "^6.0 || ^7.0 || ^8.0",
"symfony/translation-contracts": "^3.0",
"symfony/uid": "^6.0 || ^7.0 || ^8.0"
},
"suggests": {
"symfony/property-access": "Use Symfony normalizer as transformer",
"symfony/serializer": "Use Symfony normalizer as transformer"
}
}
@@ -0,0 +1,70 @@
<?php
namespace Phrity\Util\Transformer;
class BasicTypeConverter implements TransformerInterface
{
/** @var array<string|null> $supportedTypes */
private static array $supportedTypes = [
null,
Type::ARRAY,
Type::BOOLEAN,
Type::INTEGER,
Type::NULL,
Type::NUMBER,
Type::OBJECT,
Type::STRING,
];
/** @var array<string, string> $typeMap */
private array $typeMap;
/** @param array<string, string> $typeMap */
public function __construct(array $typeMap = [])
{
$this->typeMap = $typeMap;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
return in_array($type, self::$supportedTypes);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
if (!in_array(gettype($subject), self::$supportedTypes)) {
$subject = get_debug_type($subject); // Can only extract type name
}
$subjectType = gettype($subject);
$targetType = $type ?? $this->typeMap[$subjectType] ?? $subjectType;
if (!$this->canTransform($subject, $targetType)) {
throw new TransformerException("Converting '{$subjectType}' to '{$targetType}' is not supported.");
}
/** @var Type::ARRAY|Type::BOOLEAN|Type::INTEGER|Type::NULL|Type::NUMBER|Type::OBJECT|Type::STRING $targetType */
return match ($targetType) {
Type::ARRAY => (array)match ($subjectType) {
Type::OBJECT => get_object_vars($subject),
default => $subject,
},
Type::BOOLEAN => (bool)$subject,
Type::INTEGER => (int)match ($subjectType) {
Type::OBJECT => (bool)$subject,
default => $subject,
},
Type::NULL => null,
Type::NUMBER => (float)match ($subjectType) {
Type::OBJECT => (bool)$subject,
default => $subject,
},
Type::OBJECT => (object)match ($subjectType) {
Type::OBJECT => get_object_vars($subject),
default => $subject,
},
Type::STRING => (string)match ($subjectType) {
Type::ARRAY, Type::OBJECT => get_debug_type($subject),
default => $subject,
},
};
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace Phrity\Util\Transformer;
use InvalidArgumentException;
class ChainedResolver implements TransformerInterface
{
/** @var array<TransformerInterface> $transformers */
private array $transformers;
private string|null $default;
/**
* @param array<TransformerInterface> $transformers
*/
public function __construct(array $transformers, string|null $default = null)
{
foreach ($transformers as $transformer) {
if (!$transformer instanceof TransformerInterface) {
throw new InvalidArgumentException(sprintf(
"'%s' is not implementing %s",
get_debug_type($transformer),
TransformerInterface::class
));
}
}
$this->transformers = $transformers;
$this->default = $default;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
foreach ($this->transformers as $transformer) {
if ($transformer->canTransform($subject, $type)) {
return true;
}
}
return false;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$type ??= $this->default;
foreach ($this->transformers as $transformer) {
if ($transformer->canTransform($subject, $type)) {
$subject = $transformer->transform($subject, $type);
}
}
$subjectType = gettype($subject);
if ($type !== null && $type !== $subjectType) {
throw new TransformerException("Could not find transformer for '{$subjectType}'.");
}
return $subject;
}
}
@@ -0,0 +1,50 @@
<?php
namespace Phrity\Util\Transformer;
use DateInterval;
use DatePeriod;
use DateTimeInterface;
use DateTimeZone;
class DateTimeConverter implements TransformerInterface
{
public function __construct(
private string $dateTimeFormat = 'c',
private string $dateIntervalFormat = '%d',
) {
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
if (!in_array($type, [Type::STRING, null])) {
return false;
}
return $subject instanceof DateInterval
|| $subject instanceof DatePeriod
|| $subject instanceof DateTimeInterface
|| $subject instanceof DateTimeZone;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("Converting '{$subjectType}' is not supported.");
}
if ($subject instanceof DateInterval) {
return $subject->format($this->dateIntervalFormat);
}
if ($subject instanceof DatePeriod) {
return sprintf(
'%s (%s)',
$subject->getStartDate()->format($this->dateTimeFormat),
$subject->getDateInterval()->format($this->dateIntervalFormat)
);
}
if ($subject instanceof DateTimeInterface) {
return $subject->format($this->dateTimeFormat);
}
return $subject->getName();
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace Phrity\Util\Transformer;
use UnitEnum;
class EnumConverter implements TransformerInterface
{
private string|null $default;
public function __construct(bool $perDefault = false)
{
$this->default = $perDefault ? Type::STRING : null;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
return $subject instanceof UnitEnum && $type == Type::STRING;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("Enum string for '{$subjectType}' is not supported.");
}
return $subject->name;
}
}
@@ -0,0 +1,54 @@
<?php
namespace Phrity\Util\Transformer;
use InvalidArgumentException;
class FirstMatchResolver implements TransformerInterface
{
/** @var array<TransformerInterface> $transformers */
private array $transformers;
private string|null $default;
/**
* @param array<TransformerInterface> $transformers
* @param string|null $default
*/
public function __construct(array $transformers, string|null $default = null)
{
foreach ($transformers as $transformer) {
if (!$transformer instanceof TransformerInterface) {
throw new InvalidArgumentException(sprintf(
"'%s' is not implementing %s",
get_debug_type($transformer),
TransformerInterface::class
));
}
}
$this->transformers = $transformers;
$this->default = $default;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
foreach ($this->transformers as $transformer) {
if ($transformer->canTransform($subject, $type)) {
return true;
}
}
return false;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$type ??= $this->default;
foreach ($this->transformers as $transformer) {
if ($transformer->canTransform($subject, $type)) {
return $transformer->transform($subject, $type);
}
}
$subjectType = get_debug_type($subject);
throw new TransformerException("Could not find transformer for '{$subjectType}'.");
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace Phrity\Util\Transformer;
use JsonException;
class FlattenDecoder implements TransformerInterface
{
public function __construct(
private string $separator,
) {
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
return in_array($type, [null, Type::ARRAY]) && in_array(gettype($subject), [Type::ARRAY, Type::OBJECT]);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
if (empty($this->separator)) {
return $subject;
}
$re = "|^([{$this->separator}]*[^{$this->separator}]+){$this->separator}(.+)|";
$coll = [];
foreach ($subject as $key => $value) {
if ($key == $this->separator) {
$coll[$this->separator] = $value;
continue;
}
preg_match($re, $key, $res);
$keyf = $res[1] ?? $key;
$keyl = $res[2] ?? $this->separator;
$coll[$keyf][$keyl] = $value;
}
foreach ($coll as $key => $sub) {
if (!is_array($sub)) {
continue;
}
$coll[$key] = $this->transform($sub);
}
if (count($coll) === 1 && isset($coll[$this->separator])) {
return $coll[$this->separator];
}
return $coll;
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace Phrity\Util\Transformer;
use JsonException;
class JsonDecoder implements TransformerInterface
{
public function __construct(
private bool $associative = false,
) {
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
if (gettype($subject) != Type::STRING) {
return false;
}
try {
return in_array($type, [null, gettype($this->decode($subject))]);
} catch (JsonException $e) {
return false;
}
}
/**
* @throws TransformerException
*/
public function transform(mixed $subject, string|null $type = null): mixed
{
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("JSON decoding for '{$subject}' is not supported.");
}
return $this->decode($subject);
}
/**
* @throws JsonException
*/
private function decode(string $subject): mixed
{
return json_decode($subject, associative: $this->associative, flags: JSON_THROW_ON_ERROR);
}
}
@@ -0,0 +1,25 @@
<?php
namespace Phrity\Util\Transformer;
use JsonSerializable;
class JsonSerializableConverter implements TransformerInterface
{
public function canTransform(mixed $subject, string|null $type = null): bool
{
if (!$subject instanceof JsonSerializable) {
return false;
}
return in_array($type, [null, gettype($subject->jsonSerialize())]);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("JSON serialize for '{$subjectType}' is not supported.");
}
return $subject->jsonSerialize();
}
}
@@ -0,0 +1,32 @@
<?php
namespace Phrity\Util\Transformer;
class ReadableConverter implements TransformerInterface
{
private string|null $default;
public function __construct(bool $perDefault = false)
{
$this->default = $perDefault ? Type::STRING : null;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
return in_array(gettype($subject), [Type::BOOLEAN, Type::NULL]) && $type == Type::STRING;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = gettype($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("Creating readable for '{$subjectType}' is not supported.");
}
/** @var Type::BOOLEAN|Type::NULL $subjectType */
return match ($subjectType) {
Type::BOOLEAN => $subject ? 'true' : 'false',
Type::NULL => 'null',
};
}
}
@@ -0,0 +1,38 @@
<?php
namespace Phrity\Util\Transformer;
class RecursionResolver implements TransformerInterface
{
private TransformerInterface $transformer;
public function __construct(TransformerInterface $transformer)
{
$this->transformer = $transformer;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
return $this->transformer->canTransform($subject, $type);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$transformed = $this->transformer->transform($subject, $type);
if (is_array($transformed)) {
foreach ($transformed as $key => $content) {
if (is_array($content) || is_object($content)) {
$transformed[$key] = $this->transform($content, $type);
}
}
}
if (is_object($transformed)) {
foreach ((array)$transformed as $key => $content) {
if (is_array($content) || is_object($content)) {
$transformed->$key = $this->transform($content, $type);
}
}
}
return $transformed;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Phrity\Util\Transformer;
class ReversedReadableConverter implements TransformerInterface
{
public function canTransform(mixed $subject, string|null $type = null): bool
{
$subjectType = gettype($subject);
if ($subjectType != Type::STRING || !in_array($type, [Type::BOOLEAN, Type::NULL, null])) {
return false; // Must be string
}
return (in_array(strtolower($subject), ['true', 'false', '1', '0', 'null', '']));
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = gettype($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("Creating reversed readable for '{$subjectType}' is not supported.");
}
/** @var "true"|"false"|"null"|"1"|"0"|"" $subject */
$subject = strtolower($subject);
return match ($subject) {
'true', '1' => true,
'false', '0' => false,
'null' => null,
'' => $type == Type::NULL ? null : false,
};
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace Phrity\Util\Transformer;
class StringResolver implements TransformerInterface
{
private BasicTypeConverter $converter;
private TransformerInterface $transformer;
public function __construct(TransformerInterface|null $transformer = null)
{
$this->converter = new BasicTypeConverter();
$this->transformer = $transformer ?? $this->converter;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
return true;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
if ($this->transformer->canTransform($subject, $type)) {
// Convert using configured transformes and type
$subject = $this->transformer->transform($subject, $type);
}
if (is_string($subject)) {
// If we get string, done here
return $subjectType == Type::STRING ? $this->string($subject) : $subject;
}
if (is_object($subject)) {
// Force object result to array using basic type converter
$subject = $this->converter->transform($subject, Type::ARRAY);
}
if (is_array($subject)) {
// Descend into recursive worker
return $this->wrap($subject, $type, array_is_list($subject));
}
// Force non-string scalars to string
return $this->converter->transform($subject, Type::STRING);
}
/**
* @param array<array-key, mixed> $items
*/
private function wrap(array $items, string|null $type, bool $isList): string
{
return sprintf(
'%s%s%s',
$isList ? '[' : '{',
implode(', ', $this->map($items, $type, $isList)),
$isList ? ']' : '}'
);
}
/**
* @param array<array-key, mixed> $items
* @return array<array-key, mixed>
*/
private function map(array $items, string|null $type, bool $isList): array
{
return array_map(function (mixed $value, mixed $key) use ($type, $isList): mixed {
return $this->index($this->transform($value, $type), $isList ? null : $key);
}, $items, array_keys($items));
}
private function index(mixed $value, string|int|null $key = null): string
{
return is_null($key) ? $value : "{$key}: {$value}";
}
private function string(mixed $value): string
{
return is_string($value) ? "\"{$value}\"" : $value;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Phrity\Util\Transformer;
use Stringable;
class StringableConverter implements TransformerInterface
{
private string|null $default;
public function __construct(bool $perDefault = false)
{
$this->default = $perDefault ? Type::STRING : null;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
return $subject instanceof Stringable && $type == Type::STRING;
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
if (!$this->canTransform($subject, $type)) {
throw new TransformerException("Creating stringable for '{$subjectType}' is not supported.");
}
return $subject->__toString();
}
}
@@ -0,0 +1,36 @@
<?php
namespace Phrity\Util\Transformer\Symfony;
use Phrity\Util\Transformer\{
TransformerException,
TransformerInterface,
Type,
};
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class NormalizerWrapper implements TransformerInterface
{
public function __construct(
private NormalizerInterface $normalizer,
) {
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
if (!$this->normalizer->supportsNormalization($subject)) {
return false;
}
return in_array($type, [null, gettype($this->normalizer->normalize($subject))]);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
if (!$this->canTransform($subject, $type)) {
$subjectType = get_debug_type($subject);
$class = get_debug_type($this->normalizer);
throw new TransformerException("Normalizer {$class} to not support '{$subjectType}'.");
}
return $this->normalizer->normalize($subject);
}
}
@@ -0,0 +1,77 @@
<?php
namespace Phrity\Util\Transformer;
use InvalidArgumentException;
use Throwable;
class ThrowableConverter implements TransformerInterface
{
/** @var array<string|null> $supportedTypes */
private static array $supportedTypes = [
Type::ARRAY,
Type::OBJECT,
Type::STRING,
];
private string $default;
/** @var array<string> $parts */
private array $parts;
/**
* @param Type::ARRAY|Type::OBJECT|Type::STRING $default
* @param array<string> $parts
*/
public function __construct(string $default = Type::OBJECT, array $parts = ['type', 'message', 'code'])
{
if (!in_array($default, self::$supportedTypes)) {
throw new InvalidArgumentException("Invalid '{$default}' provided");
}
$this->default = $default;
$this->parts = $parts;
}
public function canTransform(mixed $subject, string|null $type = null): bool
{
$type ??= $this->default;
return $subject instanceof Throwable && in_array($type, self::$supportedTypes);
}
public function transform(mixed $subject, string|null $type = null): mixed
{
$subjectType = get_debug_type($subject);
$targetType = $type ?? $this->default;
if (!$this->canTransform($subject, $targetType)) {
throw new TransformerException("Throwable conversion for '{$subjectType}' is not supported.");
}
/** @var Type::ARRAY|Type::OBJECT|Type::STRING $targetType */
return match ($targetType) {
Type::ARRAY => $this->createData($subject),
Type::OBJECT => (object)$this->createData($subject),
Type::STRING => $subject->getMessage(),
};
}
/**
* @param Throwable $subject
* @return array<string, mixed>
*/
private function createData(Throwable $subject): array
{
$data = [];
foreach ($this->parts as $part) {
$data[$part] = match ($part) {
'type' => $subject::class,
'message' => $subject->getMessage(),
'code' => $subject->getCode(),
'file' => $subject->getFile(),
'line' => $subject->getLine(),
'trace' => $subject->getTrace(),
'previous' => $subject->getPrevious(),
default => null,
};
}
return $data;
}
}
@@ -0,0 +1,12 @@
<?php
namespace Phrity\Util\Transformer;
use LogicException;
/**
* Transformer exception class.
*/
class TransformerException extends LogicException
{
}
@@ -0,0 +1,9 @@
<?php
namespace Phrity\Util\Transformer;
interface TransformerInterface
{
public function canTransform(mixed $subject, string|null $type = null): bool;
public function transform(mixed $subject, string|null $type = null): mixed;
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace Phrity\Util\Transformer;
interface Type
{
public const ARRAY = 'array';
public const BOOLEAN = 'boolean';
public const INTEGER = 'integer';
public const NULL = 'NULL';
public const NUMBER = 'double';
public const OBJECT = 'object';
public const STRING = 'string';
}
+16
View File
@@ -0,0 +1,16 @@
# Websocket: License
Websocket PHP is free software released under the following license:
[ISC License](http://en.wikipedia.org/wiki/ISC_license)
Permission to use, copy, modify, and/or distribute this software for any purpose with or without
fee is hereby granted, provided that the above copyright notice and this permission notice appear
in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
+50
View File
@@ -0,0 +1,50 @@
{
"name": "phrity/websocket",
"type": "library",
"description": "WebSocket client and server",
"homepage": "https://phrity.sirn.se/websocket",
"keywords": ["websocket", "client", "server"],
"license": "ISC",
"authors": [
{
"name": "Fredrik Liljegren"
},
{
"name": "Sören Jensen",
"email": "sirn@sirn.se",
"homepage": "https://phrity.sirn.se"
}
],
"autoload": {
"psr-4": {
"WebSocket\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"WebSocket\\Test\\": "tests/mock/"
}
},
"require": {
"php": "^8.1",
"nyholm/psr7": "^1.4",
"phrity/http": "^1.1",
"phrity/net-uri": "^2.1",
"phrity/net-stream": "^2.4",
"psr/http-factory": "^1.0",
"psr/http-message": "^1.1 || ^2.0",
"psr/log": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^10.0 || ^11.0 || ^12.0 || ^13.0",
"phrity/logger-console": "^1.0",
"phrity/net-mock": "^2.4",
"phrity/util-errorhandler": "^1.1",
"robiningelbrecht/phpunit-coverage-tools": "^1.9",
"squizlabs/php_codesniffer": "^4.0"
},
"suggests": {
"ext-zlib": "Required for per-message deflate compression"
}
}
+764
View File
@@ -0,0 +1,764 @@
<?php
/**
* Copyright (C) 2014-2026 Textalk and contributors.
* This file is part of Websocket PHP and is free software under the ISC License.
*/
namespace WebSocket;
use InvalidArgumentException;
use Phrity\Http\HttpFactory;
use Phrity\Net\{
Context,
StreamCollection,
StreamFactory,
Uri
};
use Psr\Http\Message\{
RequestInterface,
ResponseInterface,
UriInterface,
};
use Psr\Log\{
LoggerAwareInterface,
LoggerInterface,
};
use Stringable;
use Throwable;
use WebSocket\Exception\{
BadUriException,
ClientException,
CloseException,
ConnectionLevelInterface,
ExceptionInterface,
HandshakeException,
MessageLevelInterface,
ReconnectException,
};
use WebSocket\Http\DefaultHttpFactory;
use WebSocket\Message\Message;
use WebSocket\Middleware\MiddlewareInterface;
use WebSocket\Runtime\{
IdentityInterface,
Runner,
};
use WebSocket\Trait\{
ConfigurationTrait,
ListenerTrait,
SendMethodsTrait,
StringableTrait
};
/**
* WebSocket\Client class.
* Entry class for WebSocket client.
*/
class Client implements IdentityInterface, LoggerAwareInterface, Stringable
{
use ConfigurationTrait;
/** @use ListenerTrait<Client> */
use ListenerTrait;
use SendMethodsTrait;
use StringableTrait;
private const SCOPE = 'client';
// Settings
/** @var array<string, string> $headers */
private array $headers = [];
// Internal resources
private StreamFactory $streamFactory;
private Uri $socketUri;
private Connection|null $connection = null;
/** @var array<MiddlewareInterface> $middlewares */
private array $middlewares = [];
private Runner $runner;
private bool $running = false;
private HttpFactory $httpFactory;
/** @var non-empty-string $identity */
private string $identity = 'client/<unconnected>';
/* ---------- Magic methods ------------------------------------------------------------------------------------ */
/**
* @param UriInterface|string $uri A ws/wss-URI
* @param Configuration|null $configuration
* @param StreamFactory|null $streamFactory
*/
public function __construct(
UriInterface|string $uri,
Configuration|null $configuration = null,
StreamFactory|null $streamFactory = null,
) {
$this->socketUri = $this->parseUri($uri);
$this->streamFactory = $streamFactory ?? new StreamFactory();
$this->httpFactory = new DefaultHttpFactory();
$this->identity = "client/{$this->socketUri->getHost()}";
$this->initConfiguration($configuration);
$this->runner = new Runner($this->streamFactory);
}
/**
* Get string representation of instance.
* @return string String representation
*/
public function __toString(): string
{
return $this->stringable('%s', $this->connection ? $this->socketUri->__toString() : 'closed');
}
/* ---------- Configuration ------------------------------------------------------------------------------------ */
public function getIdentity(): string
{
return $this->identity;
}
/**
* Set stream factory to use.
* @param StreamFactory $streamFactory
* @return self
* @depracated Remove in v4
*/
public function setStreamFactory(StreamFactory $streamFactory): self
{
trigger_error('Client.setStreamFactory is deprecated and will be removed in v4.', E_USER_DEPRECATED);
$this->streamFactory = $streamFactory;
return $this;
}
/**
* Set HTTP factory to use.
* @param HttpFactory $httpFactory
* @return self
*/
public function setHttpFactory(HttpFactory $httpFactory): self
{
$this->httpFactory = $httpFactory;
return $this;
}
/**
* Set logger.
* @param LoggerInterface $logger Logger implementation
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setLogger(LoggerInterface $logger): void
{
$this->configuration->setLogger($logger);
if ($this->connection) {
$this->connection->setLogger($logger);
}
}
/**
* Set timeout.
* @param int<0, max>|float $timeout Timeout in seconds
* @return self
* @throws InvalidArgumentException If invalid timeout provided
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setTimeout(int|float $timeout): self
{
$this->configuration->setTimeout($timeout);
if ($this->connection) {
$this->connection->setTimeout($timeout);
}
return $this;
}
/**
* Get timeout.
* @return int<0, max>|float Timeout in seconds
* @deprecated Will be removed in future version, get from Configuration instead
*/
public function getTimeout(): int|float
{
return $this->configuration->getTimeout();
}
/**
* Set frame size.
* @param int<1, max> $frameSize Max frame payload size in bytes
* @return self
* @throws InvalidArgumentException If invalid frameSize provided
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setFrameSize(int $frameSize): self
{
$this->configuration->setFrameSize($frameSize);
if ($this->connection) {
$this->connection->setFrameSize($frameSize);
}
return $this;
}
/**
* Get frame size.
* @return int Frame size in bytes
* @deprecated Will be removed in future version, get from Configuration instead
*/
public function getFrameSize(): int
{
return $this->configuration->getFrameSize();
}
/**
* Set connection persistence.
* @param bool $persistent True for persistent connection.
* @deprecated Will be removed in future version, set on Configuration instead
* @return self
*/
public function setPersistent(bool $persistent): self
{
$this->configuration->setPersistent($persistent);
return $this;
}
/**
* Set stream context.
* @param Context|array<string, mixed> $context Context or options as array
* @see https://www.php.net/manual/en/context.php
* @return self
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setContext(Context|array $context): self
{
if ($context instanceof Context) {
$this->configuration->setContext($context);
} else {
$this->configuration->getContext()->setOptions($context);
trigger_error('Calling Client.setContext with array is deprecated, use Context class.', E_USER_DEPRECATED);
}
return $this;
}
/**
* Get current stream context.
* @return Context
* @deprecated Will be removed in future version, get from Configuration instead
*/
public function getContext(): Context
{
return $this->configuration->getContext();
}
/**
* Add header for handshake.
* @param string $name Header name
* @param string $content Header content
* @return self
*/
public function addHeader(string $name, string $content): self
{
$this->headers[$name] = $content;
return $this;
}
/**
* Add a middleware.
* @param MiddlewareInterface $middleware
* @return self
*/
public function addMiddleware(MiddlewareInterface $middleware): self
{
$this->middlewares[] = $middleware;
if ($this->connection) {
$this->connection->addMiddleware($middleware);
}
return $this;
}
/* ---------- Messaging operations ----------------------------------------------------------------------------- */
/**
* Send message.
* @template T of Message
* @param T $message
* @return T
*/
public function send(Message $message): Message
{
return $this->connection()->pushMessage($message);
}
/**
* Receive message.
* Note that this operation will block reading.
* @return Message
*/
public function receive(): Message
{
return $this->connection()->pullMessage();
}
/* ---------- Listener operations ------------------------------------------------------------------------------ */
/**
* Start client listener.
* @throws ExceptionInterface On high level error
* @throws Throwable On low level error
*/
public function start(int|float|null $timeout = null): void
{
// Check if running
if ($this->running) {
$this->configuration->getLogger()->warning("[{scope}] Client is already running", [
'scope' => self::SCOPE,
'client' => $this->identity,
]);
return;
}
$this->running = true;
$reconnect = false;
$this->configuration->getLogger()->info("[{scope}] Client is running", [
'scope' => self::SCOPE,
'client' => $this->identity,
]);
$connection = $this->connection();
// Run handler
while ($this->running) {
try {
// Run attached handlers on selected streams
$this->runner->handle($timeout ?? $this->configuration->getTimeout());
if (!$connection->isConnected()) {
$this->running = false;
}
$connection->tick();
$this->dispatch('tick', [$this]);
} catch (CloseException $e) {
// Close connection
$connection->close($e->getCloseStatus(), $e->getMessage());
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
$this->dispatch('error', [$this, $connection, $e]);
} catch (ReconnectException $e) {
// Reconnect connection
$reconnect = true;
if ($uri = $e->getUri()) {
$this->socketUri = $uri;
}
$connection->close();
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
$this->dispatch('error', [$this, $connection, $e]);
} catch (ExceptionInterface $e) {
$this->disconnect();
$this->running = false;
// Low-level error
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
$this->dispatch('error', [$this, null, $e]);
} catch (Throwable $e) {
$this->disconnect();
$this->running = false;
// Crash it
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
throw $e;
}
gc_collect_cycles(); // Collect garbage
if ($reconnect && !$connection->isConnected()) {
$reconnect = false;
$this->running = true;
}
}
}
/**
* Stop client listener (resumable).
*/
public function stop(): void
{
$this->running = false;
$this->configuration->getLogger()->info("[{scope}] Client is stopped", [
'scope' => self::SCOPE,
'client' => $this->identity,
]);
}
/**
* If client is running (accepting messages).
* @return bool
*/
public function isRunning(): bool
{
return $this->running;
}
/* ---------- Connection management ---------------------------------------------------------------------------- */
/**
* If Client has active connection.
* @return bool True if active connection.
*/
public function isConnected(): bool
{
return $this->connection && $this->connection->isConnected();
}
/**
* If Client is readable.
* @return bool
*/
public function isReadable(): bool
{
return $this->connection && $this->connection->isReadable();
}
/**
* If Client is writable.
* @return bool
*/
public function isWritable(): bool
{
return $this->connection && $this->connection->isWritable();
}
/**
* Connect to server and perform upgrade.
* @throws ClientException On failed connection
*/
public function connect(): void
{
$this->disconnect();
$hostUri = (new Uri())
->withScheme(match ($this->socketUri->getScheme()) {
'ws', 'http' => 'tcp',
'wss', 'https' => 'ssl',
default => throw new ClientException("Invalid socket scheme: {$this->socketUri->getScheme()}")
})
->withHost($this->socketUri->getHost(Uri::IDN_ENCODE))
->withPort($this->socketUri->getPort(Uri::REQUIRE_PORT));
$stream = null;
try {
$client = $this->streamFactory->createSocketClient($hostUri, $this->configuration->getContext());
$client->setPersistent($this->configuration->isPersistent());
$client->setTimeout($this->configuration->getTimeout());
$stream = $client->connect();
} catch (Throwable $e) {
$error = "Could not open socket to \"{$hostUri}\": {$e->getMessage()}";
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
]);
throw new ClientException($error);
}
$this->connection = $connection = new Connection(
$stream,
true,
false,
$hostUri->getScheme() === 'ssl',
$this->httpFactory,
clone $this->configuration
);
$this->runner->attach($this->connection, function (Runner $runner, Connection $connection) {
try {
// Read from connection
$message = $connection->pullMessage();
$this->dispatch($message->getOpcode(), [$this, $connection, $message]);
} catch (MessageLevelInterface $e) {
// Error, but keep connection open
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
$this->dispatch('error', [$this, $connection, $e]);
} catch (ConnectionLevelInterface $e) {
// Error, disconnect connection
$this->disconnect();
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
$this->dispatch('error', [$this, $connection, $e]);
}
}, $this->connection->getIdentity());
foreach ($this->middlewares as $middleware) {
$connection->addMiddleware($middleware);
}
if (!$this->isConnected()) {
$this->configuration->getLogger()->error("[{scope}] Invalid stream on {uri}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'uri' => $hostUri,
]);
throw new ClientException("Invalid stream on \"{$hostUri}\".");
}
try {
if (!$this->configuration->isPersistent() || $stream->tell() == 0) {
/** @throws ReconnectException */
$response = $this->performHandshake($this->socketUri, $connection);
}
} catch (ReconnectException $e) {
$this->configuration->getLogger()->info("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
if ($uri = $e->getUri()) {
$this->socketUri = $uri;
}
$this->connect();
return;
}
$this->configuration->getLogger()->info("[{scope}] Client connected to {uri}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'uri' => $this->socketUri,
]);
$this->dispatch('handshake', [
$this,
$connection,
$connection->getHandshakeRequest(),
$connection->getHandshakeResponse(),
]);
$this->dispatch('connect', [$this, $connection, $connection->getHandshakeResponse()]);
}
/**
* Disconnect from server.
*/
public function disconnect(): void
{
if ($this->connection) {
$this->runner->detach($this->connection->getIdentity());
}
if ($this->connection && $this->isConnected()) {
$this->connection->disconnect();
$this->configuration->getLogger()->info("[{scope}] Client disconnected", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $this->connection->getIdentity(),
]);
$this->dispatch('disconnect', [$this, $this->connection]);
}
}
/* ---------- Connection wrapper methods ----------------------------------------------------------------------- */
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): string|null
{
return $this->isConnected() ? $this->connection?->getName() : null;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): string|null
{
return $this->isConnected() ? $this->connection?->getRemoteName() : null;
}
/**
* Get meta value on connection.
* @param string $key Meta key
* @return mixed Meta value
* @deprecated Will be removed in v4
*/
public function getMeta(string $key): mixed
{
trigger_error('Client.getMeta is deprecated and will be removed in v4.', E_USER_DEPRECATED);
return $this->isConnected() ? $this->connection?->getMeta($key) : null;
}
/**
* Get Response for handshake procedure.
* @return ResponseInterface|null Handshake.
*/
public function getHandshakeResponse(): ResponseInterface|null
{
return $this->connection ? $this->connection->getHandshakeResponse() : null;
}
/* ---------- Internal helper methods -------------------------------------------------------------------------- */
/**
* Perform upgrade handshake on new connections.
* @throws HandshakeException On failed handshake
*/
protected function performHandshake(Uri $uri, Connection $connection): ResponseInterface
{
// Generate the WebSocket key.
$key = $this->generateKey();
$request = $this->httpFactory->createRequest('GET', $uri);
$request = $request
->withHeader('User-Agent', 'websocket-client-php')
->withHeader('Connection', 'Upgrade')
->withHeader('Upgrade', 'websocket')
->withHeader('Sec-WebSocket-Key', $key)
->withHeader('Sec-WebSocket-Version', '13');
// Handle basic authentication.
if ($userinfo = $uri->getUserInfo(Uri::URI_DECODE)) {
$request = $request->withHeader('Authorization', 'Basic ' . base64_encode($userinfo));
}
// Add and override with headers.
foreach ($this->headers as $name => $content) {
$request = $request->withHeader($name, $content);
}
try {
/** @var RequestInterface */
$request = $connection->pushHttp($request);
/** @var ResponseInterface */
$response = $connection->pullHttp();
if ($response->getStatusCode() != 101) {
throw new HandshakeException("Invalid status code {$response->getStatusCode()}.", $response);
}
if (empty($response->getHeaderLine('Sec-WebSocket-Accept'))) {
throw new HandshakeException(
"Connection to '{$uri}' failed: Server sent invalid upgrade response.",
$response
);
}
$responseKey = trim($response->getHeaderLine('Sec-WebSocket-Accept'));
$expectedKey = base64_encode(
pack('H*', sha1($key . Constant::GUID))
);
if ($responseKey !== $expectedKey) {
throw new HandshakeException("Server sent bad upgrade response.", $response);
}
} catch (HandshakeException $e) {
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'exception' => $e,
'message' => $e->getMessage(),
]);
throw $e;
}
$this->configuration->getLogger()->debug("[{scope}] Handshake on {path}", [
'scope' => self::SCOPE,
'client' => $this->identity,
'connection' => $connection->getIdentity(),
'path' => $uri->getPath(),
]);
$connection->setHandshakeRequest($request);
$connection->setHandshakeResponse($response);
return $response;
}
/**
* Generate a random string for WebSocket key.
* @return string Random string
*/
protected function generateKey(): string
{
$key = '';
for ($i = 0; $i < 16; $i++) {
$key .= chr(rand(33, 126));
}
return base64_encode($key);
}
/**
* Ensure URI instance to use in client.
* @param UriInterface|string $uri A ws/wss-URI
* @return Uri
* @throws BadUriException On invalid URI
*/
protected function parseUri(UriInterface|string $uri): Uri
{
try {
if ($uri instanceof Uri) {
$uriInstance = $uri;
} elseif ($uri instanceof UriInterface) {
$uriInstance = new Uri("{$uri}");
} else {
$uriInstance = new Uri($uri);
}
} catch (InvalidArgumentException $e) {
throw new BadUriException("Invalid URI '{$uri}' provided.");
}
if (!in_array($uriInstance->getScheme(), ['ws', 'wss'])) {
throw new BadUriException("Invalid URI scheme, must be 'ws' or 'wss'.");
}
if (!$uriInstance->getHost()) {
throw new BadUriException("Invalid URI host.");
}
$uriInstance = $uriInstance->withPath($uriInstance->getPath(), Uri::ABSOLUTE_PATH | Uri::NORMALIZE_PATH);
return $uriInstance;
}
protected function connection(): Connection
{
if (!$this->isConnected()) {
$this->connect();
}
/** @var Connection */
$connection = $this->connection;
return $connection;
}
}
+195
View File
@@ -0,0 +1,195 @@
<?php
/**
* Copyright (C) 2014-2026 Textalk and contributors.
* This file is part of Websocket PHP and is free software under the ISC License.
*/
namespace WebSocket;
use InvalidArgumentException;
use Phrity\Net\Context;
use Psr\Log\{
LoggerAwareInterface,
LoggerInterface,
NullLogger,
};
use Stringable;
use WebSocket\Trait\StringableTrait;
/**
* WebSocket\Connection class.
* A client/server connection, wrapping socket stream.
*/
class Configuration implements LoggerAwareInterface, Stringable
{
use StringableTrait;
private LoggerInterface $logger;
private Context $context;
/** @var int<0, max>|float $timeout */
private int|float $timeout;
/** @var int<1, max> $frameSize */
private int $frameSize;
private bool $persistent;
/** @var int<1, max>|null $maxConnections */
private int|null $maxConnections;
/* ---------- Magic methods ------------------------------------------------------------------------------------ */
/**
* @param LoggerInterface|null $logger
* @param Context|null $context
* @param int<0, max>|float $timeout
* @param int<1, max> $frameSize
* @param bool $persistent
* @param int<1, max>|null $maxConnections
*/
public function __construct(
LoggerInterface|null $logger = null,
Context|null $context = null,
int|float|null $timeout = null,
int|null $frameSize = null,
bool|null $persistent = null,
int|null $maxConnections = null,
) {
$this->setLogger($logger ?? new NullLogger());
$this->setContext($context ?? new Context());
$this->setTimeout($timeout ?? 60);
$this->setFrameSize($frameSize ?? 4096);
$this->setPersistent($persistent ?? false);
$this->setMaxConnections($maxConnections);
}
public function __toString(): string
{
return $this->stringable('');
}
/* ---------- Logger methods ----------------------------------------------------------------------------------- */
/**
* @return LoggerInterface $logger
*/
public function getLogger(): LoggerInterface
{
return $this->logger;
}
/**
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger): void
{
$this->logger = $logger;
}
/* ---------- Context methods ---------------------------------------------------------------------------------- */
/**
* @return Context
*/
public function getContext(): Context
{
return $this->context;
}
/**
* @param Context $context Context or options as array
*/
public function setContext(Context $context): void
{
$this->context = $context;
}
/* ---------- Timeout methods ---------------------------------------------------------------------------------- */
/**
* @return int<0, max>|float Timeout in seconds
*/
public function getTimeout(): int|float
{
return $this->timeout;
}
/**
* @param int<0, max>|float $timeout Timeout in seconds
* @throws InvalidArgumentException If invalid timeout provided
*/
public function setTimeout(int|float $timeout): void
{
if ($timeout < 0) {
throw new InvalidArgumentException("Invalid timeout '{$timeout}' provided");
}
$this->timeout = $timeout;
}
/* ---------- FrameSize methods -------------------------------------------------------------------------------- */
/**
* @return int<1, max> Frame size in bytes
*/
public function getFrameSize(): int
{
return $this->frameSize;
}
/**
* @param int<1, max> $frameSize Max frame payload size in bytes
* @throws InvalidArgumentException If invalid frameSize provided
*/
public function setFrameSize(int $frameSize): void
{
if ($frameSize < 1) {
throw new InvalidArgumentException("Invalid frameSize '{$frameSize}' provided");
}
$this->frameSize = $frameSize;
}
/* ---------- Persistent methods (Client only) ----------------------------------------------------------------- */
/**
* @return bool $persistent
*/
public function isPersistent(): bool
{
return $this->persistent;
}
/**
* @param bool $persistent True for persistent connection
*/
public function setPersistent(bool $persistent): void
{
$this->persistent = $persistent;
}
/* ---------- Max connections (Server only) -------------------------------------------------------------------- */
/**
* @return int<1, max> Max connections allowed
*/
public function getMaxConnections(): int|null
{
return $this->maxConnections;
}
/**
* @param int<1, max>|null $maxConnections
* @throws InvalidArgumentException If invalid number provided
*/
public function setMaxConnections(int|null $maxConnections): void
{
if ($maxConnections !== null && $maxConnections < 1) {
throw new InvalidArgumentException("Invalid maxConnections '{$maxConnections}' provided");
}
$this->maxConnections = $maxConnections;
}
}
+498
View File
@@ -0,0 +1,498 @@
<?php
/**
* Copyright (C) 2014-2026 Textalk and contributors.
* This file is part of Websocket PHP and is free software under the ISC License.
*/
namespace WebSocket;
use InvalidArgumentException;
use Phrity\Http\HttpFactory;
use Phrity\Net\{
Context,
SocketStream,
StreamContainerInterface,
};
use Psr\Http\Message\{
MessageInterface,
RequestInterface,
ResponseFactoryInterface,
ResponseInterface,
ServerRequestFactoryInterface,
UriFactoryInterface,
};
use Psr\Log\{
LoggerAwareInterface,
LoggerInterface,
};
use Stringable;
use Throwable;
use WebSocket\Frame\FrameHandler;
use WebSocket\Http\HttpHandler;
use WebSocket\Exception\{
ConnectionClosedException,
ConnectionFailureException,
ConnectionTimeoutException,
ExceptionInterface,
ReconnectException,
};
use WebSocket\Message\{
Message,
MessageHandler
};
use WebSocket\Middleware\{
MiddlewareHandler,
MiddlewareInterface
};
use WebSocket\Runtime\IdentityInterface;
use WebSocket\Trait\{
ConfigurationTrait,
SendMethodsTrait,
StringableTrait
};
/**
* WebSocket\Connection class.
* A client/server connection, wrapping socket stream.
*/
class Connection implements IdentityInterface, LoggerAwareInterface, StreamContainerInterface, Stringable
{
use ConfigurationTrait;
use SendMethodsTrait;
use StringableTrait;
private const SCOPE = 'connection';
private SocketStream $stream;
private HttpHandler $httpHandler;
private MessageHandler $messageHandler;
private MiddlewareHandler $middlewareHandler;
private string $localName;
private string $remoteName;
private RequestInterface|null $handshakeRequest = null;
private ResponseInterface|null $handshakeResponse = null;
/** @var array<string, mixed> $meta */
private array $meta = [];
/** @var non-empty-string $identity */
private string $identity = '*/connection/<unconnected>';
/* ---------- Magic methods ------------------------------------------------------------------------------------ */
public function __construct(
SocketStream $stream,
bool $pushMasked,
bool $pullMaskedRequired,
bool $ssl = false,
HttpFactory|null $httpFactory = null,
Configuration|null $configuration = null,
) {
$this->stream = $stream;
$this->httpHandler = new HttpHandler($this->stream, $ssl, $httpFactory);
$this->messageHandler = new MessageHandler(new FrameHandler($this->stream, $pushMasked, $pullMaskedRequired));
$this->middlewareHandler = new MiddlewareHandler($this->messageHandler, $this->httpHandler);
$this->localName = $this->stream->getLocalName() ?? '<unknown>';
$this->remoteName = $this->stream->getRemoteName() ?? '<unknown>';
$this->identity = sprintf(
'*/connection/%s/%s',
$this->getIdentityPart($this->localName),
$this->getIdentityPart($this->remoteName),
);
$this->initConfiguration($configuration);
$this->stream->setTimeout($this->configuration->getTimeout());
}
public function __toString(): string
{
return $this->stringable('%s:%s', $this->localName, $this->remoteName);
}
/* ---------- Configuration ------------------------------------------------------------------------------------ */
public function getIdentity(): string
{
return $this->identity;
}
/**
* Set logger.
* @param LoggerInterface $logger Logger implementation
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setLogger(LoggerInterface $logger): void
{
$this->configuration->setLogger($logger);
$this->messageHandler->setLogger($logger);
$this->middlewareHandler->setLogger($logger);
$this->configuration->getLogger()->debug("[{scope}] Setting logger: {logger}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'logger' => get_class($logger),
]);
}
/**
* Set time out on connection.
* @param int<0, max>|float $timeout Timeout part in seconds
* @return self
* @throws InvalidArgumentException
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setTimeout(int|float $timeout): self
{
$this->configuration->setTimeout($timeout);
$this->stream->setTimeout($timeout);
$this->configuration->getLogger()->debug("[{scope}] Setting timeout: {timeout} seconds", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'timeout' => $timeout,
]);
return $this;
}
/**
* Get timeout.
* @return int<0, max>|float Timeout in seconds.
* @deprecated Will be removed in future version, get from Configuration instead
*/
public function getTimeout(): int|float
{
return $this->configuration->getTimeout();
}
/**
* Set frame size.
* @param int<1, max> $frameSize Frame size in bytes.
* @return self
* @throws InvalidArgumentException
* @deprecated Will be removed in future version, set on Configuration instead
*/
public function setFrameSize(int $frameSize): self
{
$this->configuration->setFrameSize($frameSize);
return $this;
}
/**
* Get frame size.
* @return int<1, max> Frame size in bytes
* @deprecated Will be removed in future version, get from Configuration instead
*/
public function getFrameSize(): int
{
return $this->configuration->getFrameSize();
}
/**
* Get current stream context.
* @return Context
*/
public function getContext(): Context
{
return $this->stream->getContext();
}
/**
* Add a middleware.
* @param MiddlewareInterface $middleware
* @return self
*/
public function addMiddleware(MiddlewareInterface $middleware): self
{
$this->middlewareHandler->add($middleware);
$this->configuration->getLogger()->debug("[{scope}] Added middleware: {middleware}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'middleware' => $middleware,
]);
return $this;
}
/* ---------- Connection management ---------------------------------------------------------------------------- */
/**
* If connected to stream.
* @return bool
*/
public function isConnected(): bool
{
return $this->stream->isConnected();
}
/**
* If connection is readable.
* @return bool
*/
public function isReadable(): bool
{
return $this->stream->isReadable();
}
/**
* If connection is writable.
* @return bool
*/
public function isWritable(): bool
{
return $this->stream->isWritable();
}
/**
* Close connection stream.
* @return self
*/
public function disconnect(): self
{
$this->configuration->getLogger()->info("[{scope}] Closing connection", [
'scope' => self::SCOPE,
'connection' => $this->identity,
]);
$this->stream->close();
return $this;
}
/**
* Close connection stream reading.
* @return self
*/
public function closeRead(): self
{
$this->configuration->getLogger()->info("[{scope}] Closing further reading", [
'scope' => self::SCOPE,
'connection' => $this->identity,
]);
$this->stream->closeRead();
return $this;
}
/**
* Close connection stream writing.
* @return self
*/
public function closeWrite(): self
{
$this->configuration->getLogger()->info("[{scope}] Closing further writing", [
'scope' => self::SCOPE,
'connection' => $this->identity,
]);
$this->stream->closeWrite();
return $this;
}
/* ---------- Connection state --------------------------------------------------------------------------------- */
/**
* Get name of local socket, or null if not connected.
* @return string|null
*/
public function getName(): string|null
{
return $this->localName;
}
/**
* Get name of remote socket, or null if not connected.
* @return string|null
*/
public function getRemoteName(): string|null
{
return $this->remoteName;
}
/**
* Set meta value on connection.
* @param string $key Meta key
* @param mixed $value Meta value
*/
public function setMeta(string $key, mixed $value): void
{
$this->meta[$key] = $value;
}
/**
* Get meta value on connection.
* @param string $key Meta key
* @return mixed Meta value
*/
public function getMeta(string $key): mixed
{
return $this->meta[$key] ?? null;
}
/**
* Tick operation on connection.
*/
public function tick(): void
{
$this->middlewareHandler->processTick($this);
}
/* ---------- WebSocket Message methods ------------------------------------------------------------------------ */
/**
* Send message.
* @template T of Message
* @param T $message
* @return T
*/
public function send(Message $message): Message
{
return $this->pushMessage($message);
}
/**
* Push a message to stream.
* @template T of Message
* @param T $message
* @return T
*/
public function pushMessage(Message $message): Message
{
try {
/** @throws Throwable */
return $this->middlewareHandler->processOutgoing($this, $message);
} catch (Throwable $e) {
$this->throwException($e);
}
}
/**
* Pull a message from stream
* @throws ExceptionInterface
*/
public function pullMessage(): Message
{
try {
/** @throws Throwable */
return $this->middlewareHandler->processIncoming($this);
} catch (Throwable $e) {
$this->throwException($e);
}
}
/* ---------- HTTP Message methods ----------------------------------------------------------------------------- */
public function pushHttp(MessageInterface $message): MessageInterface
{
try {
/** @throws Throwable */
return $this->middlewareHandler->processHttpOutgoing($this, $message);
} catch (Throwable $e) {
$this->throwException($e);
}
}
public function pullHttp(): MessageInterface
{
try {
/** @throws Throwable */
return $this->middlewareHandler->processHttpIncoming($this);
} catch (Throwable $e) {
$this->throwException($e);
}
}
public function setHandshakeRequest(RequestInterface $request): self
{
$this->handshakeRequest = $request;
return $this;
}
public function getHandshakeRequest(): RequestInterface|null
{
return $this->handshakeRequest;
}
public function setHandshakeResponse(ResponseInterface $response): self
{
$this->handshakeResponse = $response;
return $this;
}
public function getHandshakeResponse(): ResponseInterface|null
{
return $this->handshakeResponse;
}
public function getStream(): SocketStream
{
return $this->stream;
}
/* ---------- Internal helper methods -------------------------------------------------------------------------- */
/**
* @throws ReconnectException
* @throws ExceptionInterface
* @throws ConnectionTimeoutException
* @throws ConnectionClosedException
* @throws ConnectionFailureException
*/
protected function throwException(Throwable $e): never
{
// Internal exceptions are handled and re-thrown
if ($e instanceof ReconnectException) {
$this->configuration->getLogger()->info("[{scope}] {message}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
]);
throw $e;
}
if ($e instanceof ExceptionInterface) {
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
]);
throw $e;
}
// External exceptions are converted to internal
if ($this->isConnected()) {
$meta = $this->stream->getMetadata();
$json = json_encode($meta);
if (!empty($meta['timed_out'])) {
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
'meta' => $meta
]);
throw new ConnectionTimeoutException();
}
if (!empty($meta['eof'])) {
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
'meta' => $meta
]);
throw new ConnectionClosedException();
}
}
$this->configuration->getLogger()->error("[{scope}] {message}", [
'scope' => self::SCOPE,
'connection' => $this->identity,
'exception' => $e,
'message' => $e->getMessage(),
]);
throw new ConnectionFailureException();
}
protected function getIdentityPart(string $source): string
{
preg_match('/([0-9]+)$/', $source, $result);
return empty($result) ? $source : array_shift($result);
}
}

Some files were not shown because too many files have changed in this diff Show More