Updated PHPDocs and some argument lists.
This commit is contained in:
parent
81ad848c83
commit
51f97b7a67
63 changed files with 2058 additions and 166 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
0.2408.12207
|
||||
0.2408.12215
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Bencode.php
|
||||
// Created: 2022-01-13
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Bencode;
|
||||
|
||||
|
@ -15,44 +15,57 @@ use Index\IO\TempFileStream;
|
|||
* Provides a Bencode serialiser.
|
||||
*/
|
||||
final class Bencode {
|
||||
/**
|
||||
* Default maximum depth.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const DEFAULT_DEPTH = 512;
|
||||
|
||||
public static function encode(mixed $input, int $depth = self::DEFAULT_DEPTH): string {
|
||||
/**
|
||||
* Returns the Bencoded representation of a value.
|
||||
*
|
||||
* @param mixed $value The value being encoded. Can by string, integer, array or an object.
|
||||
* @param int $depth Set the maximum depth. Must be greater than zero.
|
||||
* @throws RuntimeException If $depth is an invalid value.
|
||||
* @return string Returns a Bencoded string.
|
||||
*/
|
||||
public static function encode(mixed $value, int $depth = self::DEFAULT_DEPTH): string {
|
||||
if($depth < 1)
|
||||
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
||||
|
||||
switch(gettype($input)) {
|
||||
switch(gettype($value)) {
|
||||
case 'string':
|
||||
return sprintf('%d:%s', strlen($input), $input);
|
||||
return sprintf('%d:%s', strlen($value), $value);
|
||||
|
||||
case 'integer':
|
||||
return sprintf('i%de', $input);
|
||||
return sprintf('i%de', $value);
|
||||
|
||||
case 'array':
|
||||
if(array_is_list($input)) {
|
||||
if(array_is_list($value)) {
|
||||
$output = 'l';
|
||||
foreach($input as $item)
|
||||
foreach($value as $item)
|
||||
$output .= self::encode($item, $depth - 1);
|
||||
} else {
|
||||
$output = 'd';
|
||||
foreach($input as $key => $value) {
|
||||
$output .= self::encode((string)$key, $depth - 1);
|
||||
$output .= self::encode($value, $depth - 1);
|
||||
foreach($value as $_key => $_value) {
|
||||
$output .= self::encode((string)$_key, $depth - 1);
|
||||
$output .= self::encode($_value, $depth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
return $output . 'e';
|
||||
|
||||
case 'object':
|
||||
if($input instanceof IBencodeSerialisable)
|
||||
return self::encode($input->bencodeSerialise(), $depth - 1);
|
||||
if($value instanceof IBencodeSerialisable)
|
||||
return self::encode($value->bencodeSerialise(), $depth - 1);
|
||||
|
||||
$input = get_object_vars($input);
|
||||
$value = get_object_vars($value);
|
||||
$output = 'd';
|
||||
|
||||
foreach($input as $key => $value) {
|
||||
$output .= self::encode((string)$key, $depth - 1);
|
||||
$output .= self::encode($value, $depth - 1);
|
||||
foreach($value as $_key => $_value) {
|
||||
$output .= self::encode((string)$_key, $depth - 1);
|
||||
$output .= self::encode($_value, $depth - 1);
|
||||
}
|
||||
|
||||
return $output . 'e';
|
||||
|
@ -62,28 +75,39 @@ final class Bencode {
|
|||
}
|
||||
}
|
||||
|
||||
public static function decode(mixed $input, int $depth = self::DEFAULT_DEPTH, bool $dictAsObject = false): mixed {
|
||||
if(is_string($input)) {
|
||||
$input = TempFileStream::fromString($input);
|
||||
$input->seek(0);
|
||||
} elseif(is_resource($input))
|
||||
$input = new GenericStream($input);
|
||||
elseif(!($input instanceof Stream))
|
||||
throw new InvalidArgumentException('$input must be a string, an Index Stream or a file resource.');
|
||||
/**
|
||||
* Decodes a bencoded string.
|
||||
*
|
||||
* @param mixed $bencoded The bencoded string being decoded. Can be an Index Stream, readable resource or a string.
|
||||
* @param int $depth Maximum nesting depth of the structure being decoded. The value must be greater than 0.
|
||||
* @param bool $associative When true, Bencoded dictionaries will be returned as associative arrays; when false, Bencoded dictionaries will be returned as objects.
|
||||
* @throws InvalidArgumentException If $bencoded string is not set to a valid type.
|
||||
* @throws InvalidArgumentException If $depth is not greater than 0.
|
||||
* @throws RuntimeException If the Bencoded stream is in an invalid state.
|
||||
* @return mixed Returns the bencoded value as an appropriate PHP type.
|
||||
*/
|
||||
public static function decode(mixed $bencoded, int $depth = self::DEFAULT_DEPTH, bool $associative = true): mixed {
|
||||
if(is_string($bencoded)) {
|
||||
$bencoded = TempFileStream::fromString($bencoded);
|
||||
$bencoded->seek(0);
|
||||
} elseif(is_resource($bencoded))
|
||||
$bencoded = new GenericStream($bencoded);
|
||||
elseif(!($bencoded instanceof Stream))
|
||||
throw new InvalidArgumentException('$bencoded must be a string, an Index Stream or a file resource.');
|
||||
|
||||
if($depth < 1)
|
||||
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
||||
throw new InvalidArgumentException('Maximum depth reached, structure is too dense.');
|
||||
|
||||
$char = $input->readChar();
|
||||
$char = $bencoded->readChar();
|
||||
if($char === null)
|
||||
throw new RuntimeException('Unexpected end of stream in $input.');
|
||||
throw new RuntimeException('Unexpected end of stream in $bencoded.');
|
||||
|
||||
switch($char) {
|
||||
case 'i':
|
||||
$number = '';
|
||||
|
||||
for(;;) {
|
||||
$char = $input->readChar();
|
||||
$char = $bencoded->readChar();
|
||||
if($char === null)
|
||||
throw new RuntimeException('Unexpected end of stream while parsing integer.');
|
||||
if($char === 'e')
|
||||
|
@ -105,13 +129,13 @@ final class Bencode {
|
|||
$list = [];
|
||||
|
||||
for(;;) {
|
||||
$char = $input->readChar();
|
||||
$char = $bencoded->readChar();
|
||||
if($char === null)
|
||||
throw new RuntimeException('Unexpected end of stream while parsing list.');
|
||||
if($char === 'e')
|
||||
break;
|
||||
$input->seek(-1, Stream::CURRENT);
|
||||
$list[] = self::decode($input, $depth - 1, $dictAsObject);
|
||||
$bencoded->seek(-1, Stream::CURRENT);
|
||||
$list[] = self::decode($bencoded, $depth - 1, $associative);
|
||||
}
|
||||
|
||||
return $list;
|
||||
|
@ -120,18 +144,18 @@ final class Bencode {
|
|||
$dict = [];
|
||||
|
||||
for(;;) {
|
||||
$char = $input->readChar();
|
||||
$char = $bencoded->readChar();
|
||||
if($char === null)
|
||||
throw new RuntimeException('Unexpected end of stream while parsing dictionary');
|
||||
if($char === 'e')
|
||||
break;
|
||||
if(!ctype_digit($char))
|
||||
throw new RuntimeException('Unexpected dictionary key type, expected a string.');
|
||||
$input->seek(-1, Stream::CURRENT);
|
||||
$dict[self::decode($input, $depth - 1, $dictAsObject)] = self::decode($input, $depth - 1, $dictAsObject);
|
||||
$bencoded->seek(-1, Stream::CURRENT);
|
||||
$dict[self::decode($bencoded, $depth - 1, $associative)] = self::decode($bencoded, $depth - 1, $associative);
|
||||
}
|
||||
|
||||
if($dictAsObject)
|
||||
if(!$associative)
|
||||
$dict = (object)$dict;
|
||||
|
||||
return $dict;
|
||||
|
@ -143,7 +167,7 @@ final class Bencode {
|
|||
$length = $char;
|
||||
|
||||
for(;;) {
|
||||
$char = $input->readChar();
|
||||
$char = $bencoded->readChar();
|
||||
if($char === null)
|
||||
throw new RuntimeException('Unexpected end of character while parsing string length.');
|
||||
if($char === ':')
|
||||
|
@ -156,7 +180,7 @@ final class Bencode {
|
|||
$length .= $char;
|
||||
}
|
||||
|
||||
return $input->read((int)$length);
|
||||
return $bencoded->read((int)$length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<?php
|
||||
// ByteFormat.php
|
||||
// Created: 2023-07-05
|
||||
// Updated: 2023-07-05
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index;
|
||||
|
||||
/**
|
||||
* Implements a byte formatter for file sizes.
|
||||
*/
|
||||
final class ByteFormat {
|
||||
/**
|
||||
* Whether the default behaviour for the format function is decimal (power of 10) or not (power of 2).
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?php
|
||||
// DataException.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2021-05-12
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception type of the Index\Data namespace.
|
||||
* Exception type for the Index\Data namespace.
|
||||
*/
|
||||
class DataException extends RuntimeException {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbStatementCache.php
|
||||
// Created: 2023-07-21
|
||||
// Updated: 2023-07-21
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data;
|
||||
|
||||
|
@ -12,6 +12,9 @@ class DbStatementCache {
|
|||
private IDbConnection $dbConn;
|
||||
private array $stmts = [];
|
||||
|
||||
/**
|
||||
* @param IDbConnection $dbConn Connection to use with this cache.
|
||||
*/
|
||||
public function __construct(IDbConnection $dbConn) {
|
||||
$this->dbConn = $dbConn;
|
||||
}
|
||||
|
@ -20,6 +23,12 @@ class DbStatementCache {
|
|||
return hash('xxh3', $query, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cached or creates a new IDbStatement instance.
|
||||
*
|
||||
* @param string $query SQL query.
|
||||
* @return IDbStatement Statement representing the query.
|
||||
*/
|
||||
public function get(string $query): IDbStatement {
|
||||
$hash = self::hash($query);
|
||||
|
||||
|
@ -32,10 +41,18 @@ class DbStatementCache {
|
|||
return $this->stmts[$hash] = $this->dbConn->prepare($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a cached statement from the cache.
|
||||
*
|
||||
* @param string $query SQL query of the statement to remove from the cache.
|
||||
*/
|
||||
public function remove(string $query): void {
|
||||
unset($this->stmts[self::hash($query)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all statement instances and resets the cache.
|
||||
*/
|
||||
public function clear(): void {
|
||||
foreach($this->stmts as $stmt)
|
||||
$stmt->close();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// DbMigrationManager.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
|
@ -15,7 +15,15 @@ use Index\Data\IDbStatement;
|
|||
use Index\Data\DbType;
|
||||
use Index\Data\SQLite\SQLiteConnection;
|
||||
|
||||
/**
|
||||
* Provides a common interface for database migrations.
|
||||
*/
|
||||
class DbMigrationManager {
|
||||
/**
|
||||
* Default table name for migrations.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const DEFAULT_TABLE = 'ndx_migrations';
|
||||
|
||||
private const CREATE_TRACK_TABLE = 'CREATE TABLE IF NOT EXISTS %1$s (migration_name %2$s PRIMARY KEY, migration_completed %2$s NOT NULL);';
|
||||
|
@ -41,33 +49,55 @@ EOF;
|
|||
private IDbStatement $checkStmt;
|
||||
private IDbStatement $insertStmt;
|
||||
|
||||
/**
|
||||
* @param IDbConnection $conn Connection to apply to migrations to.
|
||||
* @param string $tableName Name of the migration tracking table.
|
||||
*/
|
||||
public function __construct(
|
||||
private IDbConnection $conn,
|
||||
private string $tableName = self::DEFAULT_TABLE,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates the migration tracking table if it doesn't exist.
|
||||
*/
|
||||
public function createTrackingTable(): void {
|
||||
// this is not ok but it works for now, there should probably be a generic type bag alias thing
|
||||
// HACK: this is not ok but it works for now, there should probably be a generic type bag alias thing
|
||||
$nameType = $this->conn instanceof SQLiteConnection ? 'TEXT' : 'VARCHAR(255)';
|
||||
|
||||
$this->conn->execute(sprintf(self::CREATE_TRACK_TABLE, $this->tableName, $nameType));
|
||||
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the migration tracking table.
|
||||
*/
|
||||
public function destroyTrackingTable(): void {
|
||||
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares statements required for migrations.
|
||||
*/
|
||||
public function prepareStatements(): void {
|
||||
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
|
||||
$this->insertStmt = $this->conn->prepare(sprintf(self::INSERT_STMT, $this->tableName));
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all preparations required for running migrations.
|
||||
*/
|
||||
public function init(): void {
|
||||
$this->createTrackingTable();
|
||||
$this->prepareStatements();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a particular migration is present in the tracking table.
|
||||
*
|
||||
* @param string $name Name of the migration.
|
||||
* @return bool true if the migration has been run, false otherwise.
|
||||
*/
|
||||
public function checkMigration(string $name): bool {
|
||||
$this->checkStmt->reset();
|
||||
$this->checkStmt->addParameter(1, $name, DbType::STRING);
|
||||
|
@ -76,6 +106,12 @@ EOF;
|
|||
return $result->next() && !$result->isNull(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a migration as completed in the tracking table.
|
||||
*
|
||||
* @param string $name Name of the migration.
|
||||
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was run, null for now.
|
||||
*/
|
||||
public function completeMigration(string $name, ?DateTimeInterface $dateTime = null): void {
|
||||
$dateTime = XDateTime::toISO8601String($dateTime);
|
||||
|
||||
|
@ -85,10 +121,25 @@ EOF;
|
|||
$this->insertStmt->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a template for a migration PHP file.
|
||||
*
|
||||
* @param string $name Name of the migration.
|
||||
* @return string Migration template.
|
||||
*/
|
||||
public function template(string $name): string {
|
||||
return sprintf(self::TEMPLATE, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a filename for a migration PHP file.
|
||||
*
|
||||
* @param string $name Name of the migration.
|
||||
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
|
||||
* @throws InvalidArgumentException If $name is empty.
|
||||
* @throws InvalidArgumentException If $name contains invalid characters.
|
||||
* @return string Migration filename.
|
||||
*/
|
||||
public function createFileName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||
if(empty($name))
|
||||
throw new InvalidArgumentException('$name may not be empty.');
|
||||
|
@ -102,6 +153,15 @@ EOF;
|
|||
return $dateTime->format('Y_m_d_His_') . trim($name, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a class name for a migration PHP file.
|
||||
*
|
||||
* @param string $name Name of the migration.
|
||||
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
|
||||
* @throws InvalidArgumentException If $name is empty.
|
||||
* @throws InvalidArgumentException If $name contains invalid characters.
|
||||
* @return string Migration class name.
|
||||
*/
|
||||
public function createClassName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||
if(empty($name))
|
||||
throw new InvalidArgumentException('$name may not be empty.');
|
||||
|
@ -121,6 +181,13 @@ EOF;
|
|||
return $name . $dateTime->format('_Ymd_His');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate names for a PHP migration file.
|
||||
*
|
||||
* @param string $baseName Name of the migration.
|
||||
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
|
||||
* @return object Object containing names for a PHP migration file.
|
||||
*/
|
||||
public function createNames(string $baseName, ?DateTimeInterface $dateTime = null): object {
|
||||
$dateTime ??= new DateTimeImmutable('now');
|
||||
|
||||
|
@ -131,6 +198,12 @@ EOF;
|
|||
return $names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs all migrations present in a migration repository. This process is irreversible!
|
||||
*
|
||||
* @param IDbMigrationRepo $migrations Migrations repository to fetch migrations from.
|
||||
* @return array Names of migrations that have been completed.
|
||||
*/
|
||||
public function processMigrations(IDbMigrationRepo $migrations): array {
|
||||
$migrations = $migrations->getMigrations();
|
||||
$completed = [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// FsDbMigrationInfo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
|
@ -12,6 +12,9 @@ class FsDbMigrationInfo implements IDbMigrationInfo {
|
|||
private string $name;
|
||||
private string $className;
|
||||
|
||||
/**
|
||||
* @param string $path Filesystem path to the migration file.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
$this->path = $path;
|
||||
$this->name = $name = pathinfo($path, PATHINFO_FILENAME);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<?php
|
||||
// FsDbMigrationRepo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
class FsDbMigrationRepo implements IDbMigrationRepo {
|
||||
/**
|
||||
* @param string $path Filesystem path to the directory containing the migration files.
|
||||
*/
|
||||
public function __construct(
|
||||
private string $path
|
||||
) {}
|
||||
|
@ -22,6 +25,13 @@ class FsDbMigrationRepo implements IDbMigrationRepo {
|
|||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a migratinon template to a file within the directory of this migration repository.
|
||||
* If the repository directory does not exist, it will be created.
|
||||
*
|
||||
* @param string $name Name for the migration PHP file.
|
||||
* @param string $body Body for the migration PHP file.
|
||||
*/
|
||||
public function saveMigrationTemplate(string $name, string $body): void {
|
||||
if(!is_dir($this->path))
|
||||
mkdir($this->path, 0777, true);
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
<?php
|
||||
// IDbMigration.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
/**
|
||||
* Interface for migration classes to inherit.
|
||||
*/
|
||||
interface IDbMigration {
|
||||
/**
|
||||
* Runs the migration implemented by this class. This process is irreversible!
|
||||
*
|
||||
* @param IDbConnection $conn Database connection to execute this migration on.
|
||||
*/
|
||||
public function migrate(IDbConnection $conn): void;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,34 @@
|
|||
<?php
|
||||
// IDbMigrationInfo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
/**
|
||||
* Information on a migration repository item.
|
||||
*/
|
||||
interface IDbMigrationInfo {
|
||||
/**
|
||||
* Returns the name of the migration.
|
||||
*
|
||||
* @return string Migration name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Returns the class name of the migration.
|
||||
*
|
||||
* @return string Migration class name.
|
||||
*/
|
||||
public function getClassName(): string;
|
||||
|
||||
/**
|
||||
* Creates an instance of the underlying migration and runs it. This process is irreversible!
|
||||
*
|
||||
* @param IDbConnection $conn Database connection to execute this migration on.
|
||||
*/
|
||||
public function migrate(IDbConnection $conn): void;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,18 @@
|
|||
<?php
|
||||
// IDbMigrationRepo.php
|
||||
// Created: 2023-01-07
|
||||
// Updated: 2023-01-07
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Data\Migration;
|
||||
|
||||
/**
|
||||
* Represents a repository of migrations.
|
||||
*/
|
||||
interface IDbMigrationRepo {
|
||||
/**
|
||||
* Returns info on migrations contained in this repository.
|
||||
*
|
||||
* @return IDbMigrationInfo[] Collection of migration infos.
|
||||
*/
|
||||
public function getMigrations(): array;
|
||||
}
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
<?php
|
||||
// BencodedContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Stringable;
|
||||
use Index\Bencode\Bencode;
|
||||
use Index\Bencode\IBencodeSerialisable;
|
||||
use Index\IO\Stream;
|
||||
use Index\IO\FileStream;
|
||||
|
||||
class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable {
|
||||
/**
|
||||
* Represents Bencoded body content for a HTTP message.
|
||||
*/
|
||||
class BencodedContent implements IHttpContent, IBencodeSerialisable {
|
||||
private mixed $content;
|
||||
|
||||
/**
|
||||
* @param mixed $content Content to be bencoded.
|
||||
*/
|
||||
public function __construct(mixed $content) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves unencoded content.
|
||||
*
|
||||
* @return mixed Content.
|
||||
*/
|
||||
public function getContent(): mixed {
|
||||
return $this->content;
|
||||
}
|
||||
|
@ -26,6 +36,11 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
|
|||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the content.
|
||||
*
|
||||
* @return string Bencoded string.
|
||||
*/
|
||||
public function encode(): string {
|
||||
return Bencode::encode($this->content);
|
||||
}
|
||||
|
@ -34,14 +49,31 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
|
|||
return $this->encode();
|
||||
}
|
||||
|
||||
public static function fromEncoded(Stream|string $encoded): BencodedContent {
|
||||
/**
|
||||
* Creates an instance from encoded content.
|
||||
*
|
||||
* @param mixed $encoded Bencoded content.
|
||||
* @return BencodedContent Instance representing the provided content.
|
||||
*/
|
||||
public static function fromEncoded(mixed $encoded): BencodedContent {
|
||||
return new BencodedContent(Bencode::decode($encoded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from an encoded file.
|
||||
*
|
||||
* @param string $path Path to the bencoded file.
|
||||
* @return BencodedContent Instance representing the provided path.
|
||||
*/
|
||||
public static function fromFile(string $path): BencodedContent {
|
||||
return self::fromEncoded(FileStream::openRead($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from the raw request body.
|
||||
*
|
||||
* @return BencodedContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRequest(): BencodedContent {
|
||||
return self::fromFile('php://input');
|
||||
}
|
||||
|
|
|
@ -1,50 +1,102 @@
|
|||
<?php
|
||||
// FormContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2023-08-22
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\Http\HttpUploadedFile;
|
||||
|
||||
/**
|
||||
* Represents form body content for a HTTP message.
|
||||
*/
|
||||
class FormContent implements IHttpContent {
|
||||
private array $postFields;
|
||||
private array $uploadedFiles;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $postFields Form fields.
|
||||
* @param array<string, HttpUploadedFile> $uploadedFiles Uploaded files.
|
||||
*/
|
||||
public function __construct(array $postFields, array $uploadedFiles) {
|
||||
$this->postFields = $postFields;
|
||||
$this->uploadedFiles = $uploadedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a form field with filtering.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the form field, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
if(!isset($this->postFields[$name]))
|
||||
return null;
|
||||
return filter_var($this->postFields[$name] ?? null, $filter, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a form field is present.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @return bool true if the field is present, false if not.
|
||||
*/
|
||||
public function hasParam(string $name): bool {
|
||||
return isset($this->postFields[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all form fields.
|
||||
*
|
||||
* @return array<string, mixed> Form fields.
|
||||
*/
|
||||
public function getParams(): array {
|
||||
return $this->postFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all form fields as a query string.
|
||||
*
|
||||
* @param bool $spacesAsPlus true if spaces should be represented with a +, false if %20.
|
||||
* @return string Query string representation of form fields.
|
||||
*/
|
||||
public function getParamString(bool $spacesAsPlus = false): string {
|
||||
return http_build_query($this->postFields, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file upload is present.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @return bool true if the upload is present, false if not.
|
||||
*/
|
||||
public function hasUploadedFile(string $name): bool {
|
||||
return isset($this->uploadedFiles[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a file upload.
|
||||
*
|
||||
* @param string $name Name of the form field.
|
||||
* @throws RuntimeException If no uploaded file with form field $name is present.
|
||||
* @return HttpUploadedFile Uploaded file info.
|
||||
*/
|
||||
public function getUploadedFile(string $name): HttpUploadedFile {
|
||||
if(!isset($this->uploadedFiles[$name]))
|
||||
throw new RuntimeException('No file with name $name present.');
|
||||
return $this->uploadedFiles[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from an array of form fields and uploaded files.
|
||||
*
|
||||
* @param array<string, mixed> $post Form fields.
|
||||
* @param array<string, mixed> $files Uploaded files.
|
||||
* @return FormContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRaw(array $post, array $files): FormContent {
|
||||
return new FormContent(
|
||||
$post,
|
||||
|
@ -52,6 +104,11 @@ class FormContent implements IHttpContent {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from the $_POST and $_FILES superglobals.
|
||||
*
|
||||
* @return FormContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRequest(): FormContent {
|
||||
return self::fromRaw($_POST, $_FILES);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<?php
|
||||
// IHttpContent.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Stringable;
|
||||
|
||||
interface IHttpContent extends Stringable {
|
||||
}
|
||||
/**
|
||||
* Represents the body content for a HTTP message.
|
||||
*/
|
||||
interface IHttpContent extends Stringable {}
|
||||
|
|
|
@ -1,22 +1,32 @@
|
|||
<?php
|
||||
// JsonContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2023-07-21
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use JsonSerializable;
|
||||
use Stringable;
|
||||
use Index\IO\Stream;
|
||||
use Index\IO\FileStream;
|
||||
|
||||
class JsonContent implements Stringable, IHttpContent, JsonSerializable {
|
||||
/**
|
||||
* Represents JSON body content for a HTTP message.
|
||||
*/
|
||||
class JsonContent implements IHttpContent, JsonSerializable {
|
||||
private mixed $content;
|
||||
|
||||
/**
|
||||
* @param mixed $content Content to be JSON encoded.
|
||||
*/
|
||||
public function __construct(mixed $content) {
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves unencoded content.
|
||||
*
|
||||
* @return mixed Content.
|
||||
*/
|
||||
public function getContent(): mixed {
|
||||
return $this->content;
|
||||
}
|
||||
|
@ -25,6 +35,11 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
|
|||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the content.
|
||||
*
|
||||
* @return string JSON encoded string.
|
||||
*/
|
||||
public function encode(): string {
|
||||
return json_encode($this->content);
|
||||
}
|
||||
|
@ -33,14 +48,31 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
|
|||
return $this->encode();
|
||||
}
|
||||
|
||||
public static function fromEncoded(Stream|string $encoded): JsonContent {
|
||||
/**
|
||||
* Creates an instance from encoded content.
|
||||
*
|
||||
* @param mixed $encoded JSON encoded content.
|
||||
* @return JsonContent Instance representing the provided content.
|
||||
*/
|
||||
public static function fromEncoded(mixed $encoded): JsonContent {
|
||||
return new JsonContent(json_decode($encoded));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from an encoded file.
|
||||
*
|
||||
* @param string $path Path to the JSON encoded file.
|
||||
* @return JsonContent Instance representing the provided path.
|
||||
*/
|
||||
public static function fromFile(string $path): JsonContent {
|
||||
return self::fromEncoded(FileStream::openRead($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from the raw request body.
|
||||
*
|
||||
* @return JsonContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRequest(): JsonContent {
|
||||
return self::fromFile('php://input');
|
||||
}
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
<?php
|
||||
// StreamContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Stringable;
|
||||
use Index\IO\Stream;
|
||||
use Index\IO\FileStream;
|
||||
|
||||
class StreamContent implements Stringable, IHttpContent {
|
||||
/**
|
||||
* Represents Stream body content for a HTTP message.
|
||||
*/
|
||||
class StreamContent implements IHttpContent {
|
||||
private Stream $stream;
|
||||
|
||||
/**
|
||||
* @param Stream $stream Stream that represents this message body.
|
||||
*/
|
||||
public function __construct(Stream $stream) {
|
||||
$this->stream = $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the underlying stream.
|
||||
*
|
||||
* @return Stream Underlying stream.
|
||||
*/
|
||||
public function getStream(): Stream {
|
||||
return $this->stream;
|
||||
}
|
||||
|
@ -24,12 +34,23 @@ class StreamContent implements Stringable, IHttpContent {
|
|||
return (string)$this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from a file.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @return StreamContent Instance representing the provided path.
|
||||
*/
|
||||
public static function fromFile(string $path): StreamContent {
|
||||
return new StreamContent(
|
||||
FileStream::openRead($path)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from the raw request body.
|
||||
*
|
||||
* @return StreamContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRequest(): StreamContent {
|
||||
return self::fromFile('php://input');
|
||||
}
|
||||
|
|
|
@ -1,19 +1,30 @@
|
|||
<?php
|
||||
// StringContent.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Content;
|
||||
|
||||
use Stringable;
|
||||
|
||||
class StringContent implements Stringable, IHttpContent {
|
||||
/**
|
||||
* Represents string body content for a HTTP message.
|
||||
*/
|
||||
class StringContent implements IHttpContent {
|
||||
private string $string;
|
||||
|
||||
/**
|
||||
* @param string $string String that represents this message body.
|
||||
*/
|
||||
public function __construct(string $string) {
|
||||
$this->string = $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the underlying string.
|
||||
*
|
||||
* @return string Underlying string.
|
||||
*/
|
||||
public function getString(): string {
|
||||
return $this->string;
|
||||
}
|
||||
|
@ -22,14 +33,31 @@ class StringContent implements Stringable, IHttpContent {
|
|||
return $this->string;
|
||||
}
|
||||
|
||||
public static function fromObject(string $string): StringContent {
|
||||
return new StringContent($string);
|
||||
/**
|
||||
* Creates an instance an existing object.
|
||||
*
|
||||
* @param Stringable|string $string Object to cast to a string.
|
||||
* @return StringContent Instance representing the provided object.
|
||||
*/
|
||||
public static function fromObject(Stringable|string $string): StringContent {
|
||||
return new StringContent((string)$string);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from a file.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @return StringContent Instance representing the provided path.
|
||||
*/
|
||||
public static function fromFile(string $path): StringContent {
|
||||
return new StringContent(file_get_contents($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance from the raw request body.
|
||||
*
|
||||
* @return StringContent Instance representing the request body.
|
||||
*/
|
||||
public static function fromRequest(): StringContent {
|
||||
return self::fromFile('php://input');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// BencodeContentHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ContentHandling;
|
||||
|
||||
|
@ -9,6 +9,9 @@ use Index\Bencode\IBencodeSerialisable;
|
|||
use Index\Http\HttpResponseBuilder;
|
||||
use Index\Http\Content\BencodedContent;
|
||||
|
||||
/**
|
||||
* Represents a Bencode content handler for building HTTP response messages.
|
||||
*/
|
||||
class BencodeContentHandler implements IContentHandler {
|
||||
public function match(mixed $content): bool {
|
||||
return $content instanceof IBencodeSerialisable;
|
||||
|
|
|
@ -1,13 +1,29 @@
|
|||
<?php
|
||||
// IContentHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ContentHandling;
|
||||
|
||||
use Index\Http\HttpResponseBuilder;
|
||||
|
||||
/**
|
||||
* Represents a content handler for building HTTP response messages.
|
||||
*/
|
||||
interface IContentHandler {
|
||||
/**
|
||||
* Determines whether this handler is suitable for the body content.
|
||||
*
|
||||
* @param mixed $content Content to be judged.
|
||||
* @return bool true if suitable, false if not.
|
||||
*/
|
||||
function match(mixed $content): bool;
|
||||
|
||||
/**
|
||||
* Handles body content.
|
||||
*
|
||||
* @param HttpResponseBuilder $response Response to apply this body to.
|
||||
* @param mixed $content Body to apply to the response message.
|
||||
*/
|
||||
function handle(HttpResponseBuilder $response, mixed $content): void;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// JsonContentHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ContentHandling;
|
||||
|
||||
|
@ -10,6 +10,9 @@ use JsonSerializable;
|
|||
use Index\Http\HttpResponseBuilder;
|
||||
use Index\Http\Content\JsonContent;
|
||||
|
||||
/**
|
||||
* Represents a JSON content handler for building HTTP response messages.
|
||||
*/
|
||||
class JsonContentHandler implements IContentHandler {
|
||||
public function match(mixed $content): bool {
|
||||
return is_array($content) || $content instanceof JsonSerializable || $content instanceof stdClass;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// StreamContentHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ContentHandling;
|
||||
|
||||
|
@ -9,6 +9,9 @@ use Index\Http\HttpResponseBuilder;
|
|||
use Index\Http\Content\StreamContent;
|
||||
use Index\IO\Stream;
|
||||
|
||||
/**
|
||||
* Represents a Stream content handler for building HTTP response messages.
|
||||
*/
|
||||
class StreamContentHandler implements IContentHandler {
|
||||
public function match(mixed $content): bool {
|
||||
return $content instanceof Stream;
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
// HtmlErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ErrorHandling;
|
||||
|
||||
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
||||
|
||||
/**
|
||||
* Represents a basic HTML error message handler for building HTTP response messages.
|
||||
*/
|
||||
class HtmlErrorHandler implements IErrorHandler {
|
||||
private const TEMPLATE = <<<HTML
|
||||
<!doctype html>
|
||||
|
|
|
@ -1,13 +1,24 @@
|
|||
<?php
|
||||
// IErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ErrorHandling;
|
||||
|
||||
use Index\Http\HttpResponseBuilder;
|
||||
use Index\Http\HttpRequest;
|
||||
|
||||
/**
|
||||
* Represents an error message handler for building HTTP response messages.
|
||||
*/
|
||||
interface IErrorHandler {
|
||||
/**
|
||||
* Applies an error message template to the provided HTTP response builder.
|
||||
*
|
||||
* @param HttpResponseBuilder $response HTTP Response builder to apply this error to.
|
||||
* @param HttpRequest $request HTTP Request this error is a response to.
|
||||
* @param int $code HTTP status code to apply.
|
||||
* @param string $message HTTP status message to apply.
|
||||
*/
|
||||
function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
<?php
|
||||
// PlainErrorHandler.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\ErrorHandling;
|
||||
|
||||
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
||||
|
||||
/**
|
||||
* Represents a plain text error message handler for building HTTP response messages.
|
||||
*/
|
||||
class PlainErrorHandler implements IErrorHandler {
|
||||
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
|
||||
$response->setTypePlain();
|
||||
|
|
|
@ -1,29 +1,51 @@
|
|||
<?php
|
||||
// HttpHeader.php
|
||||
// Created: 2022-02-14
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Represents a generic HTTP header.
|
||||
*/
|
||||
class HttpHeader implements Stringable {
|
||||
private string $name;
|
||||
private array $lines;
|
||||
|
||||
/**
|
||||
* @param string $name Name of the header.
|
||||
* @param string ...$lines Lines of the header.
|
||||
*/
|
||||
public function __construct(string $name, string ...$lines) {
|
||||
$this->name = $name;
|
||||
$this->lines = $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the header.
|
||||
*
|
||||
* @return string Header name.
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves lines of the header.
|
||||
*
|
||||
* @return string[] Header lines.
|
||||
*/
|
||||
public function getLines(): array {
|
||||
return $this->lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the first line of the header.
|
||||
*
|
||||
* @return string First header line.
|
||||
*/
|
||||
public function getFirstLine(): string {
|
||||
return $this->lines[0];
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
<?php
|
||||
// HttpHeaders.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents a collection of HTTP headers.
|
||||
*/
|
||||
class HttpHeaders {
|
||||
private array $headers;
|
||||
|
||||
/**
|
||||
* @param HttpHeader[] $headers HTTP header instances.
|
||||
*/
|
||||
public function __construct(array $headers) {
|
||||
$real = [];
|
||||
|
||||
|
@ -20,14 +26,32 @@ class HttpHeaders {
|
|||
$this->headers = $real;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header is present.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return bool true if the header is present.
|
||||
*/
|
||||
public function hasHeader(string $name): bool {
|
||||
return isset($this->headers[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all headers.
|
||||
*
|
||||
* @return HttpHeader[] All headers.
|
||||
*/
|
||||
public function getHeaders(): array {
|
||||
return array_values($this->headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a header by name.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @throws RuntimeException If no header with $name exists.
|
||||
* @return HttpHeader Instance of the requested header.
|
||||
*/
|
||||
public function getHeader(string $name): HttpHeader {
|
||||
$name = strtolower($name);
|
||||
if(!isset($this->headers[$name]))
|
||||
|
@ -36,18 +60,36 @@ class HttpHeaders {
|
|||
return $this->headers[$name];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of a header as a string.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string Contents of the header.
|
||||
*/
|
||||
public function getHeaderLine(string $name): string {
|
||||
if(!$this->hasHeader($name))
|
||||
return '';
|
||||
return (string)$this->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets lines of a header.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string[] Header lines.
|
||||
*/
|
||||
public function getHeaderLines(string $name): array {
|
||||
if(!$this->hasHeader($name))
|
||||
return [];
|
||||
return $this->getHeader($name)->getLines();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first line of a header.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string First line of the header.
|
||||
*/
|
||||
public function getHeaderFirstLine(string $name): string {
|
||||
if(!$this->hasHeader($name))
|
||||
return '';
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<?php
|
||||
// HttpHeadersBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a HTTP message header builder.
|
||||
*/
|
||||
class HttpHeadersBuilder {
|
||||
private array $headers = [];
|
||||
|
||||
/**
|
||||
* Adds a header to the HTTP message.
|
||||
* If a header with the same name is already present, it will be appended with a new line.
|
||||
*
|
||||
* @param string $name Name of the header to add.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
*/
|
||||
public function addHeader(string $name, mixed $value): void {
|
||||
$nameLower = strtolower($name);
|
||||
if(!isset($this->headers[$nameLower]))
|
||||
|
@ -17,18 +27,41 @@ class HttpHeadersBuilder {
|
|||
$this->headers[$nameLower][] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a header to the HTTP message.
|
||||
* If a header with the same name is already present, it will be overwritten.
|
||||
*
|
||||
* @param string $name Name of the header to set.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
*/
|
||||
public function setHeader(string $name, mixed $value): void {
|
||||
$this->headers[strtolower($name)] = [$name, $value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a header from the HTTP message.
|
||||
*
|
||||
* @param string $name Name of the header to remove.
|
||||
*/
|
||||
public function removeHeader(string $name): void {
|
||||
unset($this->headers[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header is already present.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return bool true if it is present.
|
||||
*/
|
||||
public function hasHeader(string $name): bool {
|
||||
return isset($this->headers[strtolower($name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HttpHeaders instance from this builder.
|
||||
*
|
||||
* @return HttpHeaders Instance containing HTTP headers.
|
||||
*/
|
||||
public function toHeaders(): HttpHeaders {
|
||||
$headers = [];
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<?php
|
||||
// HttpMessage.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use RuntimeException;
|
||||
use Index\IO\Stream;
|
||||
use Index\Http\Content\IHttpContent;
|
||||
use Index\Http\Content\BencodedContent;
|
||||
|
@ -13,69 +14,153 @@ use Index\Http\Content\JsonContent;
|
|||
use Index\Http\Content\StreamContent;
|
||||
use Index\Http\Content\StringContent;
|
||||
|
||||
/**
|
||||
* Represents a base HTTP message.
|
||||
*/
|
||||
abstract class HttpMessage {
|
||||
private string $version;
|
||||
private HttpHeaders $headers;
|
||||
private ?IHttpContent $content;
|
||||
|
||||
/**
|
||||
* @param string $version HTTP message version.
|
||||
* @param HttpHeaders $headers HTTP message headers.
|
||||
* @param ?IHttpContent $content Body contents.
|
||||
*/
|
||||
public function __construct(string $version, HttpHeaders $headers, ?IHttpContent $content) {
|
||||
$this->version = $version;
|
||||
$this->headers = $headers;
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP version of this message.
|
||||
*
|
||||
* @return string HTTP version.
|
||||
*/
|
||||
public function getHttpVersion(): string {
|
||||
return $this->version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the collection of headers for this HTTP message.
|
||||
*
|
||||
* @return HttpHeader[] HTTP headers.
|
||||
*/
|
||||
public function getHeaders(): array {
|
||||
return $this->headers->getHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header is present.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return bool true if the header is present.
|
||||
*/
|
||||
public function hasHeader(string $name): bool {
|
||||
return $this->headers->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a header by name.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @throws RuntimeException If no header with $name exists.
|
||||
* @return HttpHeader Instance of the requested header.
|
||||
*/
|
||||
public function getHeader(string $name): HttpHeader {
|
||||
return $this->headers->getHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the contents of a header as a string.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string Contents of the header.
|
||||
*/
|
||||
public function getHeaderLine(string $name): string {
|
||||
return $this->headers->getHeaderLine($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets lines of a header.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string[] Header lines.
|
||||
*/
|
||||
public function getHeaderLines(string $name): array {
|
||||
return $this->headers->getHeaderLines($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first line of a header.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return string First line of the header.
|
||||
*/
|
||||
public function getHeaderFirstLine(string $name): string {
|
||||
return $this->headers->getHeaderFirstLine($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this HTTP message has body contents.
|
||||
*
|
||||
* @return bool true if it has body contents.
|
||||
*/
|
||||
public function hasContent(): bool {
|
||||
return $this->content !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves message body contents, if present.
|
||||
*
|
||||
* @return ?IHttpContent Body contents, null if none present.
|
||||
*/
|
||||
public function getContent(): ?IHttpContent {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the body content is JsonContent.
|
||||
*
|
||||
* @return bool true if it is JsonContent.
|
||||
*/
|
||||
public function isJsonContent(): bool {
|
||||
return $this->content instanceof JsonContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the body content is FormContent.
|
||||
*
|
||||
* @return bool true if it is FormContent.
|
||||
*/
|
||||
public function isFormContent(): bool {
|
||||
return $this->content instanceof FormContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the body content is StreamContent.
|
||||
*
|
||||
* @return bool true if it is StreamContent.
|
||||
*/
|
||||
public function isStreamContent(): bool {
|
||||
return $this->content instanceof StreamContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the body content is StringContent.
|
||||
*
|
||||
* @return bool true if it is StringContent.
|
||||
*/
|
||||
public function isStringContent(): bool {
|
||||
return $this->content instanceof StringContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the body content is BencodedContent.
|
||||
*
|
||||
* @return bool true if it is BencodedContent.
|
||||
*/
|
||||
public function isBencodedContent(): bool {
|
||||
return $this->content instanceof BencodedContent;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpMessageBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -10,6 +10,9 @@ use Index\Http\Content\IHttpContent;
|
|||
use Index\Http\Content\StreamContent;
|
||||
use Index\Http\Content\StringContent;
|
||||
|
||||
/**
|
||||
* Represents a base HTTP message builder.
|
||||
*/
|
||||
class HttpMessageBuilder {
|
||||
private ?string $version = null;
|
||||
private HttpHeadersBuilder $headers;
|
||||
|
@ -19,47 +22,107 @@ class HttpMessageBuilder {
|
|||
$this->headers = new HttpHeadersBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP version of the message.
|
||||
*
|
||||
* @return string HTTP version.
|
||||
*/
|
||||
protected function getHttpVersion(): string {
|
||||
return $this->version ?? '1.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP version for the message.
|
||||
*
|
||||
* @param string $version HTTP version.
|
||||
*/
|
||||
public function setHttpVersion(string $version): void {
|
||||
$this->version = $version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create HttpHeaders instance from this builder.
|
||||
*
|
||||
* @return HttpHeaders Instance containing HTTP headers.
|
||||
*/
|
||||
protected function getHeaders(): HttpHeaders {
|
||||
return $this->headers->toHeaders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves instance of the underlying HTTP header builder.
|
||||
*
|
||||
* @return HttpHeadersBuilder HTTP header builder.
|
||||
*/
|
||||
public function getHeadersBuilder(): HttpHeadersBuilder {
|
||||
return $this->headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a header to the HTTP message.
|
||||
* If a header with the same name is already present, it will be appended with a new line.
|
||||
*
|
||||
* @param string $name Name of the header to add.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
*/
|
||||
public function addHeader(string $name, mixed $value): void {
|
||||
$this->headers->addHeader($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a header to the HTTP message.
|
||||
* If a header with the same name is already present, it will be overwritten.
|
||||
*
|
||||
* @param string $name Name of the header to set.
|
||||
* @param mixed $value Value to apply for this header.
|
||||
*/
|
||||
public function setHeader(string $name, mixed $value): void {
|
||||
$this->headers->setHeader($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a header from the HTTP message.
|
||||
*
|
||||
* @param string $name Name of the header to remove.
|
||||
*/
|
||||
public function removeHeader(string $name): void {
|
||||
$this->headers->removeHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a header is already present.
|
||||
*
|
||||
* @param string $name Name of the header.
|
||||
* @return bool true if it is present.
|
||||
*/
|
||||
public function hasHeader(string $name): bool {
|
||||
return $this->headers->hasHeader($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves HTTP message body content.
|
||||
*
|
||||
* @return ?IHttpContent Body content.
|
||||
*/
|
||||
protected function getContent(): ?IHttpContent {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/** @phpstan-impure */
|
||||
/**
|
||||
* Checks whether this HTTP message has body contents.
|
||||
*
|
||||
* @return bool true if it has body contents.
|
||||
* @phpstan-impure
|
||||
*/
|
||||
public function hasContent(): bool {
|
||||
return $this->content !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP message body contents.
|
||||
*
|
||||
* @param IHttpContent|Stream|string|null $content Body contents
|
||||
*/
|
||||
public function setContent(IHttpContent|Stream|string|null $content): void {
|
||||
if($content instanceof Stream)
|
||||
$content = new StreamContent($content);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRequest.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -13,12 +13,24 @@ use Index\Http\Content\JsonContent;
|
|||
use Index\Http\Content\StreamContent;
|
||||
use Index\Http\Content\FormContent;
|
||||
|
||||
/**
|
||||
* Represents a HTTP request message.
|
||||
*/
|
||||
class HttpRequest extends HttpMessage {
|
||||
private string $method;
|
||||
private string $path;
|
||||
private array $params;
|
||||
private array $cookies;
|
||||
|
||||
/**
|
||||
* @param string $version HTTP message version.
|
||||
* @param string $method HTTP request method.
|
||||
* @param string $path HTTP request path.
|
||||
* @param array<string, mixed> $params HTTP request query parameters.
|
||||
* @param array<string, string> $cookies HTTP request cookies.
|
||||
* @param HttpHeaders $headers HTTP message headers.
|
||||
* @param ?IHttpContent $content Body contents.
|
||||
*/
|
||||
public function __construct(
|
||||
string $version,
|
||||
string $method,
|
||||
|
@ -36,46 +48,105 @@ class HttpRequest extends HttpMessage {
|
|||
parent::__construct($version, $headers, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP request method.
|
||||
*
|
||||
* @return string HTTP request method.
|
||||
*/
|
||||
public function getMethod(): string {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP request path.
|
||||
*
|
||||
* @return string HTTP request path.
|
||||
*/
|
||||
public function getPath(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all HTTP request query fields as a query string.
|
||||
*
|
||||
* @param bool $spacesAsPlus true if spaces should be represented with a +, false if %20.
|
||||
* @return string Query string representation of query fields.
|
||||
*/
|
||||
public function getParamString(bool $spacesAsPlus = false): string {
|
||||
return http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all HTTP request query fields.
|
||||
*
|
||||
* @return array<string, mixed> Query fields.
|
||||
*/
|
||||
public function getParams(): array {
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an HTTP request query field, or null if it is not present.
|
||||
*
|
||||
* @param string $name Name of the request query field.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the query field, null if not present.
|
||||
*/
|
||||
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
if(!isset($this->params[$name]))
|
||||
return null;
|
||||
return filter_var($this->params[$name] ?? null, $filter, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a query field is present.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
* @return bool true if the field is present, false if not.
|
||||
*/
|
||||
public function hasParam(string $name): bool {
|
||||
return isset($this->params[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all HTTP request cookies.
|
||||
*
|
||||
* @return array<string, string> All cookies.
|
||||
*/
|
||||
public function getCookies(): array {
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves an HTTP request cookie, or null if it is not present.
|
||||
*
|
||||
* @param string $name Name of the request cookie.
|
||||
* @param int $filter A PHP filter extension filter constant.
|
||||
* @param array|int $options Options for the PHP filter.
|
||||
* @return mixed Value of the cookie, null if not present.
|
||||
*/
|
||||
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||
if(!isset($this->cookies[$name]))
|
||||
return null;
|
||||
return filter_var($this->cookies[$name] ?? null, $filter, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a cookie is present.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @return bool true if the cookie is present, false if not.
|
||||
*/
|
||||
public function hasCookie(string $name): bool {
|
||||
return isset($this->cookies[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HttpRequest instance from the current request.
|
||||
*
|
||||
* @return HttpRequest An instance representing the current request.
|
||||
*/
|
||||
public static function fromRequest(): HttpRequest {
|
||||
$build = new HttpRequestBuilder;
|
||||
$build->setHttpVersion($_SERVER['SERVER_PROTOCOL']);
|
||||
|
|
|
@ -1,64 +1,134 @@
|
|||
<?php
|
||||
// HttpRequestBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
/**
|
||||
* Represents a HTTP request message builder.
|
||||
*/
|
||||
class HttpRequestBuilder extends HttpMessageBuilder {
|
||||
private string $method = 'GET';
|
||||
private string $path = '/';
|
||||
private array $params = [];
|
||||
private array $cookies = [];
|
||||
|
||||
/**
|
||||
* Returns HTTP request method.
|
||||
*
|
||||
* @return string HTTP request method.
|
||||
*/
|
||||
protected function getMethod(): string {
|
||||
return $this->method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP request method.
|
||||
*
|
||||
* @param string $method HTTP request method.
|
||||
*/
|
||||
public function setMethod(string $method): void {
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP request path.
|
||||
*
|
||||
* @return string HTTP request path.
|
||||
*/
|
||||
protected function getPath(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP request path.
|
||||
*
|
||||
* @param string $path HTTP request path.
|
||||
*/
|
||||
public function setPath(string $path): void {
|
||||
$this->path = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP request query params.
|
||||
*
|
||||
* @return array<string, mixed> HTTP request query params.
|
||||
*/
|
||||
protected function getParams(): array {
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP request query params.
|
||||
*
|
||||
* @param array<string, mixed> $params HTTP request query params.
|
||||
*/
|
||||
public function setParams(array $params): void {
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a HTTP request query param.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
* @param mixed $value Value of the query field.
|
||||
*/
|
||||
public function setParam(string $name, mixed $value): void {
|
||||
$this->params[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a HTTP request query param.
|
||||
*
|
||||
* @param string $name Name of the query field.
|
||||
*/
|
||||
public function removeParam(string $name): void {
|
||||
unset($this->params[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns HTTP request cookies.
|
||||
*
|
||||
* @return array<string, string> HTTP request cookies.
|
||||
*/
|
||||
protected function getCookies(): array {
|
||||
return $this->cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets HTTP request cookies.
|
||||
*
|
||||
* @param array<string, string> $cookies HTTP request cookies.
|
||||
*/
|
||||
public function setCookies(array $cookies): void {
|
||||
$this->cookies = $cookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param mixed $value Value of the cookie.
|
||||
*/
|
||||
public function setCookie(string $name, mixed $value): void {
|
||||
$this->cookies[$name] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a HTTP request cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
*/
|
||||
public function removeCookie(string $name): void {
|
||||
unset($this->cookies[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HttpRequest instance from this builder.
|
||||
*
|
||||
* @return HttpRequest An instance representing this builder.
|
||||
*/
|
||||
public function toRequest(): HttpRequest {
|
||||
return new HttpRequest(
|
||||
$this->getHttpVersion(),
|
||||
|
|
|
@ -1,16 +1,26 @@
|
|||
<?php
|
||||
// HttpResponse.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
use Index\Http\Content\IHttpContent;
|
||||
|
||||
/**
|
||||
* Represents a HTTP response message.
|
||||
*/
|
||||
class HttpResponse extends HttpMessage {
|
||||
private int $statusCode;
|
||||
private string $statusText;
|
||||
|
||||
/**
|
||||
* @param string $version HTTP message version.
|
||||
* @param int $statusCode HTTP response status code.
|
||||
* @param string $statusText HTTP response status text.
|
||||
* @param HttpHeaders $headers HTTP message headers.
|
||||
* @param ?IHttpContent $content Body contents.
|
||||
*/
|
||||
public function __construct(
|
||||
string $version,
|
||||
int $statusCode,
|
||||
|
@ -24,10 +34,20 @@ class HttpResponse extends HttpMessage {
|
|||
parent::__construct($version, $headers, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP response message status code.
|
||||
*
|
||||
* @return int HTTP status code.
|
||||
*/
|
||||
public function getStatusCode(): int {
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP response message status text.
|
||||
*
|
||||
* @return string HTTP status text.
|
||||
*/
|
||||
public function getStatusText(): string {
|
||||
return $this->statusText;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpResponseBuilder.php
|
||||
// Created: 2022-02-08
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -11,35 +11,78 @@ use Index\MediaType;
|
|||
use Index\XDateTime;
|
||||
use Index\Performance\Timings;
|
||||
|
||||
/**
|
||||
* Represents a HTTP response message builder.
|
||||
*/
|
||||
class HttpResponseBuilder extends HttpMessageBuilder {
|
||||
private int $statusCode = -1;
|
||||
private ?string $statusText;
|
||||
private array $vary = [];
|
||||
|
||||
/**
|
||||
* Retrieves the HTTP status code for the target HTTP response message.
|
||||
*
|
||||
* @return int HTTP status code.
|
||||
*/
|
||||
public function getStatusCode(): int {
|
||||
return $this->statusCode < 0 ? 200 : $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP status code for the target HTTP response message.
|
||||
*
|
||||
* @param int $statusCode HTTP status code.
|
||||
*/
|
||||
public function setStatusCode(int $statusCode): void {
|
||||
$this->statusCode = $statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this response message builder has a status code.
|
||||
*
|
||||
* @return bool true if it has a status code greater than and including 100.
|
||||
*/
|
||||
public function hasStatusCode(): bool {
|
||||
return $this->statusCode >= 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the status text for this response.
|
||||
*
|
||||
* @return string Status text.
|
||||
*/
|
||||
public function getStatusText(): string {
|
||||
return $this->statusText ?? self::STATUS[$this->getStatusCode()] ?? 'Unknown Status';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status text for this response.
|
||||
*
|
||||
* @param string $statusText Status text.
|
||||
*/
|
||||
public function setStatusText(string $statusText): void {
|
||||
$this->statusText = (string)$statusText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the status text for this message.
|
||||
*/
|
||||
public function clearStatusText(): void {
|
||||
$this->statusText = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a cookie to this response.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param mixed $value Value of the cookie.
|
||||
* @param DateTimeInterface|int|null $expires Point at which the cookie should expire.
|
||||
* @param string $path Path to which to apply the cookie.
|
||||
* @param string $domain Domain name to which to apply the cookie.
|
||||
* @param bool $secure true to only make the client include this cookie in a secure context.
|
||||
* @param bool $httpOnly true to make the client hide this cookie to Javascript code.
|
||||
* @param bool $sameSiteStrict true to set the SameSite attribute to Strict, false to set it to Lax.
|
||||
*/
|
||||
public function addCookie(
|
||||
string $name,
|
||||
mixed $value,
|
||||
|
@ -71,6 +114,16 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
$this->addHeader('Set-Cookie', $cookie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make the client remove a cookie.
|
||||
*
|
||||
* @param string $name Name of the cookie.
|
||||
* @param string $path Path to which the cookie was applied to cookie.
|
||||
* @param string $domain Domain name to which the cookie was applied to cookie.
|
||||
* @param bool $secure true to only make the client include this cookie in a secure context.
|
||||
* @param bool $httpOnly true to make the client hide this cookie to Javascript code.
|
||||
* @param bool $sameSiteStrict true to set the SameSite attribute to Strict, false to set it to Lax.
|
||||
*/
|
||||
public function removeCookie(
|
||||
string $name,
|
||||
string $path = '',
|
||||
|
@ -82,11 +135,22 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
$this->addCookie($name, '', -9001, $path, $domain, $secure, $httpOnly, $sameSiteStrict);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a redirect header on the response message.
|
||||
*
|
||||
* @param string $to Target to redirect to.
|
||||
* @param bool $permanent true for status code 301, false for status code 302. Makes the client cache the redirect.
|
||||
*/
|
||||
public function redirect(string $to, bool $permanent = false): void {
|
||||
$this->setStatusCode($permanent ? 301 : 302);
|
||||
$this->setHeader('Location', $to);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Vary header.
|
||||
*
|
||||
* @param string|array $headers Header or headers that will vary.
|
||||
*/
|
||||
public function addVary(string|array $headers): void {
|
||||
if(!is_array($headers))
|
||||
$headers = [$headers];
|
||||
|
@ -100,10 +164,21 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
$this->setHeader('Vary', implode(', ', $this->vary));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an X-Powered-By header.
|
||||
*
|
||||
* @param string $poweredBy Thing that your website is powered by.
|
||||
*/
|
||||
public function setPoweredBy(string $poweredBy): void {
|
||||
$this->setHeader('X-Powered-By', $poweredBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an ETag header.
|
||||
*
|
||||
* @param string $eTag Unique identifier for the current state of the content.
|
||||
* @param bool $weak Whether to use weak matching.
|
||||
*/
|
||||
public function setEntityTag(string $eTag, bool $weak = false): void {
|
||||
$eTag = '"' . $eTag . '"';
|
||||
|
||||
|
@ -113,6 +188,11 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
$this->setHeader('ETag', $eTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Server-Timing header.
|
||||
*
|
||||
* @param Timings $timings Timings to supply to the devtools.
|
||||
*/
|
||||
public function setServerTiming(Timings $timings): void {
|
||||
$laps = $timings->getLaps();
|
||||
$timings = [];
|
||||
|
@ -129,50 +209,109 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
$this->setHeader('Server-Timing', implode(', ', $timings));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a Content-Type header is present.
|
||||
*
|
||||
* @return bool true if a Content-Type header is present.
|
||||
*/
|
||||
public function hasContentType(): bool {
|
||||
return $this->hasHeader('Content-Type');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a Content-Type header.
|
||||
*
|
||||
* @param MediaType|string $mediaType Media type to set as the content type of the response body.
|
||||
*/
|
||||
public function setContentType(MediaType|string $mediaType): void {
|
||||
$this->setHeader('Content-Type', (string)$mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'application/octet-stream' for raw content.
|
||||
*/
|
||||
public function setTypeStream(): void {
|
||||
$this->setContentType('application/octet-stream');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'text/plain' with a provided character set for plain text content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypePlain(string $charset = 'us-ascii'): void {
|
||||
$this->setContentType('text/plain; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'text/html' with a provided character set for HTML content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeHTML(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('text/html; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'application/json' with a provided character set for JSON content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeJson(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/json; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'application/xml' with a provided character set for XML content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeXML(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/xml; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'text/css' with a provided character set for CSS content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeCSS(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('text/css; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Content-Type to 'application/javascript' with a provided character set for Javascript content.
|
||||
*
|
||||
* @param string $charset Character set.
|
||||
*/
|
||||
public function setTypeJS(string $charset = 'utf-8'): void {
|
||||
$this->setContentType('application/javascript; charset=' . $charset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies an Apache Web Server X-Sendfile header.
|
||||
*
|
||||
* @param string $absolutePath Absolute path to the content to serve.
|
||||
*/
|
||||
public function sendFile(string $absolutePath): void {
|
||||
$this->setHeader('X-Sendfile', $absolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies an NGINX X-Accel-Redirect header.
|
||||
*
|
||||
* @param string $uri Relative URI to the content to serve.
|
||||
*/
|
||||
public function accelRedirect(string $uri): void {
|
||||
$this->setHeader('X-Accel-Redirect', $uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a Content-Disposition header to supply a filename for the content.
|
||||
*
|
||||
* @param string $fileName Name of the file.
|
||||
* @param bool $attachment true if the browser should prompt the user to download the body.
|
||||
*/
|
||||
public function setFileName(string $fileName, bool $attachment = false): void {
|
||||
$this->setHeader(
|
||||
'Content-Disposition',
|
||||
|
@ -184,6 +323,11 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a Clear-Site-Data header.
|
||||
*
|
||||
* @param string ...$directives Directives to specify.
|
||||
*/
|
||||
public function clearSiteData(string ...$directives): void {
|
||||
$this->setHeader(
|
||||
'Clear-Site-Data',
|
||||
|
@ -191,10 +335,20 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a Cache-Control header.
|
||||
*
|
||||
* @param string ...$directives Directives to specify.
|
||||
*/
|
||||
public function setCacheControl(string ...$directives): void {
|
||||
$this->setHeader('Cache-Control', implode(', ', $directives));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of HttpResponse from this builder.
|
||||
*
|
||||
* @return HttpResponse An instance representing this builder.
|
||||
*/
|
||||
public function toResponse(): HttpResponse {
|
||||
return new HttpResponse(
|
||||
$this->getHttpVersion(),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpUploadedFile.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Index\IO\FileStream;
|
|||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Represents an uploaded file in a multipart/form-data request.
|
||||
*/
|
||||
class HttpUploadedFile implements ICloseable {
|
||||
private int $errorCode;
|
||||
private int $size;
|
||||
|
@ -21,6 +24,13 @@ class HttpUploadedFile implements ICloseable {
|
|||
private bool $hasMoved = false;
|
||||
private ?Stream $stream = null;
|
||||
|
||||
/**
|
||||
* @param int $errorCode PHP file upload error code.
|
||||
* @param int $size Size, as provided by the client.
|
||||
* @param string $localFileName Filename generated by PHP on the server.
|
||||
* @param string $suggestedFileName Filename included by the client.
|
||||
* @param MediaType|string $suggestedMediaType Mediatype included by the client.
|
||||
*/
|
||||
public function __construct(
|
||||
int $errorCode,
|
||||
int $size,
|
||||
|
@ -38,34 +48,75 @@ class HttpUploadedFile implements ICloseable {
|
|||
$this->suggestedMediaType = $suggestedMediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the PHP file upload error code.
|
||||
*
|
||||
* @return int PHP file upload error code.
|
||||
*/
|
||||
public function getErrorCode(): int {
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the size of the uploaded file.
|
||||
*
|
||||
* @return int Size of uploaded file.
|
||||
*/
|
||||
public function getSize(): int {
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the local path to the uploaded file.
|
||||
*
|
||||
* @return ?string Path to file, or null.
|
||||
*/
|
||||
public function getLocalFileName(): ?string {
|
||||
return $this->localFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves media type of the uploaded file.
|
||||
*
|
||||
* @return ?MediaType Type of file, or null.
|
||||
*/
|
||||
public function getLocalMediaType(): ?MediaType {
|
||||
return MediaType::fromPath($this->localFileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the suggested name for the uploaded file.
|
||||
*
|
||||
* @return string Suggested name for the file.
|
||||
*/
|
||||
public function getSuggestedFileName(): string {
|
||||
return $this->suggestedFileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the suggested media type for the uploaded file.
|
||||
*
|
||||
* @return MediaType Suggested type for the file.
|
||||
*/
|
||||
public function getSuggestedMediaType(): MediaType {
|
||||
return $this->suggestedMediaType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the file has been moved to its final destination.
|
||||
*
|
||||
* @return bool true if it has been moved.
|
||||
*/
|
||||
public function hasMoved(): bool {
|
||||
return $this->hasMoved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a read-only stream to the uploaded file.
|
||||
*
|
||||
* @throws RuntimeException If the file cannot be opened.
|
||||
* @return Stream Read-only stream of the upload file.
|
||||
*/
|
||||
public function getStream(): Stream {
|
||||
if($this->stream === null) {
|
||||
if($this->errorCode !== UPLOAD_ERR_OK)
|
||||
|
@ -79,6 +130,15 @@ class HttpUploadedFile implements ICloseable {
|
|||
return $this->stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the uploaded file to its final destination.
|
||||
*
|
||||
* @param string $path Path to move the file to.
|
||||
* @throws RuntimeException If the file has already been moved.
|
||||
* @throws RuntimeException If an upload error occurred.
|
||||
* @throws InvalidArgumentException If the provided $path is not valid.
|
||||
* @throws RuntimeException If the file failed to move.
|
||||
*/
|
||||
public function moveTo(string $path): void {
|
||||
if($this->hasMoved)
|
||||
throw new RuntimeException('This uploaded file has already been moved.');
|
||||
|
@ -110,6 +170,11 @@ class HttpUploadedFile implements ICloseable {
|
|||
$this->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a HttpUploadedFile instance from an entry in the $_FILES superglobal.
|
||||
*
|
||||
* @return HttpUploadedFile Uploaded file info.
|
||||
*/
|
||||
public static function createFromFILE(array $file): self {
|
||||
return new HttpUploadedFile(
|
||||
$file['error'] ?? UPLOAD_ERR_NO_FILE,
|
||||
|
@ -120,6 +185,11 @@ class HttpUploadedFile implements ICloseable {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a collection of HttpUploadedFile instances from the $_FILES superglobal.
|
||||
*
|
||||
* @return array<string, mixed> Uploaded files.
|
||||
*/
|
||||
public static function createFromFILES(array $files): array {
|
||||
if(empty($files))
|
||||
return [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpDelete.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpDelete extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('DELETE', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpGet.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpGet extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('GET', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpOptions.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpOptions extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('OPTIONS', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpPatch.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpPatch extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('PATCH', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpPost.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpPost extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('POST', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpPut.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -12,6 +12,9 @@ use Attribute;
|
|||
*/
|
||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||
class HttpPut extends HttpRoute {
|
||||
/**
|
||||
* @param string $path Path this route represents.
|
||||
*/
|
||||
public function __construct(string $path) {
|
||||
parent::__construct('PUT', $path);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// HttpRouter.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
|
@ -26,6 +26,11 @@ class HttpRouter implements IRouter {
|
|||
|
||||
private IErrorHandler $errorHandler;
|
||||
|
||||
/**
|
||||
* @param string $charSet Default character set to specify when none is present.
|
||||
* @param IErrorHandler|string $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
|
||||
* @param bool $registerDefaultContentHandlers true to register default content handlers for JSON, Bencode, etc.
|
||||
*/
|
||||
public function __construct(
|
||||
string $charSet = '',
|
||||
IErrorHandler|string $errorHandler = 'html',
|
||||
|
@ -38,16 +43,31 @@ class HttpRouter implements IRouter {
|
|||
$this->registerDefaultContentHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the normalised name of the preferred character set.
|
||||
*
|
||||
* @return string Normalised character set name.
|
||||
*/
|
||||
public function getCharSet(): string {
|
||||
if($this->defaultCharSet === '')
|
||||
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
|
||||
return $this->defaultCharSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the error handler instance.
|
||||
*
|
||||
* @return IErrorHandler The error handler.
|
||||
*/
|
||||
public function getErrorHandler(): IErrorHandler {
|
||||
return $this->errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active error handler.
|
||||
*
|
||||
* @param IErrorHandler|string $handler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
|
||||
*/
|
||||
public function setErrorHandler(IErrorHandler|string $handler): void {
|
||||
if($handler instanceof IErrorHandler)
|
||||
$this->errorHandler = $handler;
|
||||
|
@ -57,25 +77,45 @@ class HttpRouter implements IRouter {
|
|||
$this->setPlainErrorHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the basic HTML one.
|
||||
*/
|
||||
public function setHTMLErrorHandler(): void {
|
||||
$this->errorHandler = new HtmlErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the error handler to the plain text one.
|
||||
*/
|
||||
public function setPlainErrorHandler(): void {
|
||||
$this->errorHandler = new PlainErrorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a message body content handler.
|
||||
*
|
||||
* @param IContentHandler $contentHandler Content handler to register.
|
||||
*/
|
||||
public function registerContentHandler(IContentHandler $contentHandler): void {
|
||||
if(!in_array($contentHandler, $this->contentHandlers))
|
||||
$this->contentHandlers[] = $contentHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the default content handlers.
|
||||
*/
|
||||
public function registerDefaultContentHandlers(): void {
|
||||
$this->registerContentHandler(new StreamContentHandler);
|
||||
$this->registerContentHandler(new JsonContentHandler);
|
||||
$this->registerContentHandler(new BencodeContentHandler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a scoped router to a given path prefix.
|
||||
*
|
||||
* @param string $prefix Prefix to apply to paths within the returned router.
|
||||
* @return IRouter Scopes router proxy.
|
||||
*/
|
||||
public function scopeTo(string $prefix): IRouter {
|
||||
return new ScopedRouter($this, $prefix);
|
||||
}
|
||||
|
@ -92,6 +132,12 @@ class HttpRouter implements IRouter {
|
|||
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a middleware handler.
|
||||
*
|
||||
* @param string $path Path prefix or regex to apply this middleware on.
|
||||
* @param callable $handler Middleware handler.
|
||||
*/
|
||||
public function use(string $path, callable $handler): void {
|
||||
$this->middlewares[] = $mwInfo = new stdClass;
|
||||
$mwInfo->handler = $handler;
|
||||
|
@ -105,6 +151,15 @@ class HttpRouter implements IRouter {
|
|||
$mwInfo->prefix = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a route handler for a given method and path.
|
||||
*
|
||||
* @param string $method Method to use this handler for.
|
||||
* @param string $path Path or regex to use this handler with.
|
||||
* @param callable $handler Handler to use for this method/path combination.
|
||||
* @throws InvalidArgumentException If $method is empty.
|
||||
* @throws InvalidArgumentException If $method starts or ends with spaces.
|
||||
*/
|
||||
public function add(string $method, string $path, callable $handler): void {
|
||||
if($method === '')
|
||||
throw new InvalidArgumentException('$method may not be empty');
|
||||
|
@ -130,6 +185,13 @@ class HttpRouter implements IRouter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves middlewares and a route handler for a given method and path.
|
||||
*
|
||||
* @param string $method Method to resolve for.
|
||||
* @param string $path Path to resolve for.
|
||||
* @return ResolvedRouteInfo Resolved route information.
|
||||
*/
|
||||
public function resolve(string $method, string $path): ResolvedRouteInfo {
|
||||
if(str_ends_with($path, '/'))
|
||||
$path = substr($path, 0, -1);
|
||||
|
@ -178,6 +240,12 @@ class HttpRouter implements IRouter {
|
|||
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
|
||||
*
|
||||
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
|
||||
* @param array $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
|
||||
*/
|
||||
public function dispatch(?HttpRequest $request = null, array $args = []): void {
|
||||
$request ??= HttpRequest::fromRequest();
|
||||
$response = new HttpResponseBuilder;
|
||||
|
@ -232,11 +300,24 @@ class HttpRouter implements IRouter {
|
|||
self::output($response->toResponse(), $request->getMethod() !== 'HEAD');
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an error page to a given HTTP response builder.
|
||||
*
|
||||
* @param HttpResponseBuilder $response HTTP response builder to apply the error page to.
|
||||
* @param HttpRequest $request HTTP request that triggered this error.
|
||||
* @param int $statusCode HTTP status code for this error page.
|
||||
*/
|
||||
public function writeErrorPage(HttpResponseBuilder $response, HttpRequest $request, int $statusCode): void {
|
||||
$response->setStatusCode($statusCode);
|
||||
$this->errorHandler->handle($response, $request, $response->getStatusCode(), $response->getStatusText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs a HTTP response message to stdout.
|
||||
*
|
||||
* @param HttpResponse $response HTTP response message to output.
|
||||
* @param bool $includeBody true to include the response message body, false to omit it for HEAD requests.
|
||||
*/
|
||||
public static function output(HttpResponse $response, bool $includeBody): void {
|
||||
header(sprintf(
|
||||
'HTTP/%s %03d %s',
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
<?php
|
||||
// ResolvedRouteInfo.php
|
||||
// Created: 2024-03-28
|
||||
// Updated: 2024-03-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Http\Routing;
|
||||
|
||||
/**
|
||||
* Represents a resolved route.
|
||||
*/
|
||||
class ResolvedRouteInfo {
|
||||
/**
|
||||
* @param array $middlewares Middlewares that should be run prior to the route handler.
|
||||
* @param array $supportedMethods HTTP methods that this route accepts.
|
||||
* @param mixed $handler Route handler.
|
||||
* @param array $args Argument list to pass to the middleware and route handlers.
|
||||
*/
|
||||
public function __construct(
|
||||
private array $middlewares,
|
||||
private array $supportedMethods,
|
||||
|
@ -13,6 +22,12 @@ class ResolvedRouteInfo {
|
|||
private array $args,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Run middleware handlers.
|
||||
*
|
||||
* @param array $args Additional arguments to pass to the middleware handlers.
|
||||
* @return mixed Return value from the first middleware to return anything non-null, otherwise null.
|
||||
*/
|
||||
public function runMiddleware(array $args): mixed {
|
||||
foreach($this->middlewares as $middleware) {
|
||||
$result = $middleware[0](...array_merge($args, $middleware[1]));
|
||||
|
@ -23,18 +38,39 @@ class ResolvedRouteInfo {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this route has a handler.
|
||||
*
|
||||
* @return bool true if it does.
|
||||
*/
|
||||
public function hasHandler(): bool {
|
||||
return $this->handler !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this route supports other HTTP request methods.
|
||||
*
|
||||
* @return bool true if it does.
|
||||
*/
|
||||
public function hasOtherMethods(): bool {
|
||||
return !empty($this->supportedMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list of supported HTTP request methods for this route.
|
||||
*
|
||||
* @return string[] Supported HTTP request methods.
|
||||
*/
|
||||
public function getSupportedMethods(): array {
|
||||
return $this->supportedMethods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches this route.
|
||||
*
|
||||
* @param array $args Additional arguments to pass to the route handler.
|
||||
* @return mixed Return value of the route handler.
|
||||
*/
|
||||
public function dispatch(array $args): mixed {
|
||||
return ($this->handler)(...array_merge($args, $this->args));
|
||||
}
|
||||
|
|
|
@ -1,31 +1,121 @@
|
|||
<?php
|
||||
// FileStream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use ErrorException;
|
||||
|
||||
/**
|
||||
* Represents a Stream representing a file.
|
||||
*/
|
||||
class FileStream extends GenericStream {
|
||||
public const OPEN_READ = 'rb'; // O R E // Open file for reading, throw if not exists
|
||||
public const OPEN_READ_WRITE = 'r+b'; // O RW E // Open file for reading and writing, throw if not exist
|
||||
public const NEW_WRITE = 'wb'; // C W T // Create file for writing, truncate if exist
|
||||
public const NEW_READ_WRITE = 'w+b'; // C RW T // Create file for reading and writing, truncate if exist
|
||||
public const APPEND_WRITE = 'ab'; // OC A // Open file for appending, create if not exist
|
||||
public const APPEND_READ_WRITE = 'a+b'; // OC R A // Open file for reading and appending, create if not exist
|
||||
public const CREATE_WRITE = 'xb'; // C W T // Create file for writing, throw if exist
|
||||
public const CREATE_READ_WRITE = 'x+b'; // C RW T // Create file for reading and writing, throw if exist
|
||||
public const OPEN_OR_CREATE_WRITE = 'cb'; // OC W // Opens or creates a file for writing
|
||||
public const OPEN_OR_CREATE_READ_WRITE = 'c+b'; // OC RW // Opens or creates a file for reading and writing
|
||||
/**
|
||||
* Open file for reading, throw if not exists.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const OPEN_READ = 'rb';
|
||||
|
||||
public const LOCK_NONE = 0;
|
||||
public const LOCK_READ = LOCK_SH;
|
||||
public const LOCK_WRITE = LOCK_EX;
|
||||
/**
|
||||
* Open file for reading and writing, throw if not exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const OPEN_READ_WRITE = 'r+b';
|
||||
|
||||
/**
|
||||
* Create file for writing, truncate if exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const NEW_WRITE = 'wb';
|
||||
|
||||
/**
|
||||
* Create file for reading and writing, truncate if exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const NEW_READ_WRITE = 'w+b';
|
||||
|
||||
/**
|
||||
* Open file for appending, create if not exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const APPEND_WRITE = 'ab';
|
||||
|
||||
/**
|
||||
* Open file for reading and appending, create if not exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const APPEND_READ_WRITE = 'a+b';
|
||||
|
||||
/**
|
||||
* Create file for writing, throw if exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CREATE_WRITE = 'xb';
|
||||
|
||||
/**
|
||||
* Create file for reading and writing, throw if exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CREATE_READ_WRITE = 'x+b';
|
||||
|
||||
/**
|
||||
* Opens or creates a file for writing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const OPEN_OR_CREATE_WRITE = 'cb';
|
||||
|
||||
/**
|
||||
* Opens or creates a file for reading and writing.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const OPEN_OR_CREATE_READ_WRITE = 'c+b';
|
||||
|
||||
/**
|
||||
* Don't lock file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LOCK_NONE = 0;
|
||||
|
||||
/**
|
||||
* Lock for reading.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LOCK_READ = LOCK_SH;
|
||||
|
||||
/**
|
||||
* Lock for writing.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LOCK_WRITE = LOCK_EX;
|
||||
|
||||
/**
|
||||
* Don't block during lock attempt.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const LOCK_NON_BLOCKING = LOCK_NB;
|
||||
|
||||
private int $lock;
|
||||
|
||||
/**
|
||||
* @param string $path Path to the file.
|
||||
* @param string $mode Stream mode to use to open the file.
|
||||
* @param int $lock Locking method to use.
|
||||
*/
|
||||
public function __construct(string $path, string $mode, int $lock) {
|
||||
$this->lock = $lock;
|
||||
|
||||
|
@ -52,33 +142,112 @@ class FileStream extends GenericStream {
|
|||
parent::close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file for reading, throw if not exists.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function openRead(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::OPEN_READ, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file for reading and writing, throw if not exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function openReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::OPEN_READ_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file for writing, truncate if exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function newWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::NEW_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file for reading and writing, truncate if exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function newReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::NEW_READ_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file for appending, create if not exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function appendWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::APPEND_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open file for reading and appending, create if not exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function appendReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::APPEND_READ_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file for writing, throw if exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function createWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::CREATE_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create file for reading and writing, throw if exist.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function createReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::CREATE_READ_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or creates a file for writing.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function openOrCreateWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::OPEN_OR_CREATE_WRITE, $lock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens or creates a file for reading and writing.
|
||||
*
|
||||
* @param string $path Path to the file.
|
||||
* @param int $lock Locking method to use.
|
||||
* @return FileStream Stream to file.
|
||||
*/
|
||||
public static function openOrCreateReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||
return new FileStream($path, self::OPEN_OR_CREATE_READ_WRITE, $lock);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,31 @@
|
|||
<?php
|
||||
// GenericStream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2023-07-17
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents any stream that can be handled using the C-like f* functions.
|
||||
*/
|
||||
class GenericStream extends Stream {
|
||||
/**
|
||||
* Readable file modes.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public const READABLE = [
|
||||
'r', 'rb', 'r+', 'r+b', 'w+', 'w+b',
|
||||
'a+', 'a+b', 'x+', 'x+b', 'c+', 'c+b',
|
||||
];
|
||||
|
||||
/**
|
||||
* Writeable file modes.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public const WRITEABLE = [
|
||||
'r+', 'r+b', 'w', 'wb', 'w+', 'w+b',
|
||||
'a', 'ab', 'a+', 'a+b', 'x', 'xb',
|
||||
|
@ -24,6 +38,9 @@ class GenericStream extends Stream {
|
|||
private bool $canWrite;
|
||||
private bool $canSeek;
|
||||
|
||||
/**
|
||||
* @param resource $stream Resource that this stream represents.
|
||||
*/
|
||||
public function __construct($stream) {
|
||||
if(!is_resource($stream) || get_resource_type($stream) !== 'stream')
|
||||
throw new InvalidArgumentException('$stream is not a valid stream resource.');
|
||||
|
@ -35,6 +52,11 @@ class GenericStream extends Stream {
|
|||
$this->canSeek = $metaData['seekable'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying resource handle.
|
||||
*
|
||||
* @return resource Underlying resource handle.
|
||||
*/
|
||||
public function getResource() {
|
||||
return $this->stream;
|
||||
}
|
||||
|
@ -98,8 +120,8 @@ class GenericStream extends Stream {
|
|||
return $buffer;
|
||||
}
|
||||
|
||||
public function seek(int $offset, int $origin = Stream::START): int {
|
||||
return fseek($this->stream, $offset, $origin);
|
||||
public function seek(int $offset, int $origin = Stream::START): bool {
|
||||
return fseek($this->stream, $offset, $origin) === 0;
|
||||
}
|
||||
|
||||
public function write(string $buffer, int $length = -1): void {
|
||||
|
@ -123,6 +145,7 @@ class GenericStream extends Stream {
|
|||
} catch(\Error $ex) {}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function copyTo(Stream $other): void {
|
||||
if($other instanceof GenericStream) {
|
||||
stream_copy_to_stream($this->stream, $other->stream);
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<?php
|
||||
// IOException.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2021-04-30
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Exception type for the Index\IO namespace.
|
||||
*/
|
||||
class IOException extends RuntimeException {}
|
||||
|
|
|
@ -1,15 +1,24 @@
|
|||
<?php
|
||||
// MemoryStream.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
/**
|
||||
* Represents an in-memory stream.
|
||||
*/
|
||||
class MemoryStream extends GenericStream {
|
||||
public function __construct() {
|
||||
parent::__construct(fopen('php://memory', 'r+b'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a memory stream from a string/binary data.
|
||||
*
|
||||
* @param string $string String to create the stream from.
|
||||
* @return MemoryStream Stream representing the string.
|
||||
*/
|
||||
public static function fromString(string $string): MemoryStream {
|
||||
$stream = new MemoryStream;
|
||||
$stream->write($string);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// NetworkStream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
|
@ -9,7 +9,16 @@ use ErrorException;
|
|||
use Index\Net\IPAddress;
|
||||
use Index\Net\EndPoint;
|
||||
|
||||
/**
|
||||
* Represents a network socket stream.
|
||||
*/
|
||||
class NetworkStream extends GenericStream {
|
||||
/**
|
||||
* @param string $hostname Hostname to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
* @throws IOException If the socket failed to open.
|
||||
*/
|
||||
public function __construct(string $hostname, int $port, float|null $timeout) {
|
||||
try {
|
||||
$stream = fsockopen($hostname, $port, $errcode, $errmsg, $timeout);
|
||||
|
@ -23,41 +32,118 @@ class NetworkStream extends GenericStream {
|
|||
parent::__construct($stream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the network stream should have blocking reads and writes.
|
||||
*
|
||||
* @param bool $blocking true to block, false to async.
|
||||
*/
|
||||
public function setBlocking(bool $blocking): void {
|
||||
stream_set_blocking($this->stream, $blocking);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a network socket stream to an endpoint represented by an Index EndPoint instance.
|
||||
*
|
||||
* @param EndPoint $endPoint Host to connect to.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openEndPoint(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream((string)$endPoint, -1, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SSL network socket stream to an endpoint represented by an Index EndPoint instance.
|
||||
*
|
||||
* @param EndPoint $endPoint Host to connect to.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openEndPointSSL(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('ssl://' . ((string)$endPoint), -1, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a TLS network socket stream to an endpoint represented by an Index EndPoint instance.
|
||||
*
|
||||
* @param EndPoint $endPoint Host to connect to.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openEndPointTLS(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('tls://' . ((string)$endPoint), -1, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a network socket stream to an endpoint represented by an hostname and port.
|
||||
*
|
||||
* @param string $hostname Hostname to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openHost(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream($hostname, $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SSL network socket stream to an endpoint represented by an hostname and port.
|
||||
*
|
||||
* @param string $hostname Hostname to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openHostSSL(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('ssl://' . ((string)$hostname), $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a TLS network socket stream to an endpoint represented by an hostname and port.
|
||||
*
|
||||
* @param string $hostname Hostname to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openHostTLS(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('tls://' . ((string)$hostname), $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a network socket stream to an endpoint represented by an Index IPAddress instance and port.
|
||||
*
|
||||
* @param IPAddress $address Address to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openAddress(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream($address->getAddress(), $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens an SSL network socket stream to an endpoint represented by an Index IPAddress instance and port.
|
||||
*
|
||||
* @param IPAddress $address Address to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openAddressSSL(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('ssl://' . $address->getAddress(), $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a TLS network socket stream to an endpoint represented by an Index IPAddress instance and port.
|
||||
*
|
||||
* @param IPAddress $address Address to connect to.
|
||||
* @param int $port Port to connect at.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openAddressTLS(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('tls://' . $address->getAddress(), $port, $timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a network socket stream to an endpoint represented by a UNIX socket path.
|
||||
*
|
||||
* @param string $path Path to connect to.
|
||||
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
|
||||
*/
|
||||
public static function openUnix(string $path, float|null $timeout = null): NetworkStream {
|
||||
return new NetworkStream('unix://' . ((string)$path), -1, $timeout);
|
||||
return new NetworkStream('unix://' . $path, -1, $timeout);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
<?php
|
||||
// ProcessStream.php
|
||||
// Created: 2023-01-25
|
||||
// Updated: 2023-01-25
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
/**
|
||||
* Represents a stream to a running sub-process.
|
||||
*/
|
||||
class ProcessStream extends Stream {
|
||||
private $handle;
|
||||
private bool $canRead;
|
||||
private bool $canWrite;
|
||||
|
||||
/**
|
||||
* @param string $command Command to run for this stream.
|
||||
* @param string $mode File mode to use.
|
||||
* @throws IOException If we were unable to spawn the process.
|
||||
*/
|
||||
public function __construct(string $command, string $mode) {
|
||||
$this->handle = popen($command, $mode);
|
||||
$this->canRead = strpos($mode, 'r') !== false;
|
||||
|
@ -71,7 +79,7 @@ class ProcessStream extends Stream {
|
|||
return $buffer;
|
||||
}
|
||||
|
||||
public function seek(int $offset, int $origin = self::START): int {
|
||||
public function seek(int $offset, int $origin = self::START): bool {
|
||||
throw new IOException('Cannot seek ProcessStream.');
|
||||
}
|
||||
|
||||
|
|
|
@ -1,37 +1,113 @@
|
|||
<?php
|
||||
// Stream.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
use Stringable;
|
||||
use Index\ICloseable;
|
||||
|
||||
/**
|
||||
* Represents a generic data stream.
|
||||
*/
|
||||
abstract class Stream implements Stringable, ICloseable {
|
||||
/**
|
||||
* Place the cursor relative to the start of the file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const START = SEEK_SET;
|
||||
|
||||
/**
|
||||
* Place the cursor relative to the current position of the cursor.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const CURRENT = SEEK_CUR;
|
||||
|
||||
/**
|
||||
* Place the cursor relative to the end of the file.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const END = SEEK_END;
|
||||
|
||||
/**
|
||||
* Retrieves the current cursor position.
|
||||
*
|
||||
* @return int Cursor position.
|
||||
*/
|
||||
abstract public function getPosition(): int;
|
||||
|
||||
/**
|
||||
* Retrieves the current length of the stream.
|
||||
*
|
||||
* @return int Stream length.
|
||||
*/
|
||||
abstract public function getLength(): int;
|
||||
|
||||
/**
|
||||
* Sets the length of the stream.
|
||||
*
|
||||
* @param int $length Desired stream length.
|
||||
*/
|
||||
abstract public function setLength(int $length): void;
|
||||
|
||||
/**
|
||||
* Whether this stream is readable.
|
||||
*
|
||||
* @return bool true if the stream is readable.
|
||||
*/
|
||||
abstract public function canRead(): bool;
|
||||
|
||||
/**
|
||||
* Whether this stream is writeable.
|
||||
*
|
||||
* @return bool true if the stream is writeable.
|
||||
*/
|
||||
abstract public function canWrite(): bool;
|
||||
|
||||
/**
|
||||
* Whether this stream is seekable.
|
||||
*
|
||||
* @return bool true if the stream is seekable.
|
||||
*/
|
||||
abstract public function canSeek(): bool;
|
||||
|
||||
/**
|
||||
* Whether this stream has timed out.
|
||||
*
|
||||
* @return bool true if the stream has timed out.
|
||||
*/
|
||||
abstract public function hasTimedOut(): bool;
|
||||
|
||||
/**
|
||||
* Whether this stream has blocking operations.
|
||||
*
|
||||
* @return bool true if the stream has blocking operations.
|
||||
*/
|
||||
abstract public function isBlocking(): bool;
|
||||
|
||||
/**
|
||||
* Whether the cursor is at the end of the file.
|
||||
*
|
||||
* @return bool true if the cursor is at the end of the file.
|
||||
*/
|
||||
abstract public function isEnded(): bool;
|
||||
|
||||
/**
|
||||
* Read a single character from the stream.
|
||||
*
|
||||
* @return ?string One character or null if we're at the end of the stream.
|
||||
*/
|
||||
abstract public function readChar(): ?string;
|
||||
|
||||
/**
|
||||
* Read a single byte from the stream.
|
||||
*
|
||||
* @return int One byte or -1 if we're at the end of the stream.
|
||||
*/
|
||||
public function readByte(): int {
|
||||
$char = $this->readChar();
|
||||
if($char === null)
|
||||
|
@ -39,25 +115,72 @@ abstract class Stream implements Stringable, ICloseable {
|
|||
return ord($char);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a line of text from the stream.
|
||||
*
|
||||
* @return ?string One line of text or null if we're at the end of the stream.
|
||||
*/
|
||||
abstract public function readLine(): ?string;
|
||||
|
||||
/**
|
||||
* Read an amount of data from the stream.
|
||||
*
|
||||
* @param int $length Maximum amount of data to read.
|
||||
* @return ?string At most a $length sized string or null if we're at the end of the stream.
|
||||
*/
|
||||
abstract public function read(int $length): ?string;
|
||||
|
||||
abstract public function seek(int $offset, int $origin = self::START): int;
|
||||
/**
|
||||
* Move the cursor to a different place in the stream.
|
||||
*
|
||||
* @param int $offset Offset to apply to the cursor position.
|
||||
* @param int $origin Point from which to apply the offset.
|
||||
* @throws IOException If the stream is not seekable.
|
||||
* @return bool true if the cursor offset was applied successfully.
|
||||
*/
|
||||
abstract public function seek(int $offset, int $origin = self::START): bool;
|
||||
|
||||
/**
|
||||
* Write an amount of data to the stream.
|
||||
*
|
||||
* @param string $buffer Buffer to write from.
|
||||
* @param int $length Amount of data to write from the buffer, or -1 to write the entire buffer.
|
||||
*/
|
||||
abstract public function write(string $buffer, int $length = -1): void;
|
||||
|
||||
/**
|
||||
* Writes a line of text to the stream.
|
||||
*
|
||||
* @param string $line Line of text to write.
|
||||
*/
|
||||
public function writeLine(string $line): void {
|
||||
$this->write($line);
|
||||
$this->write(PHP_EOL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single character to the stream.
|
||||
*
|
||||
* @param string $char Character to write to the stream.
|
||||
*/
|
||||
abstract public function writeChar(string $char): void;
|
||||
|
||||
/**
|
||||
* Writes a single byte to the stream.
|
||||
*
|
||||
* @param int $byte Byte to write to the stream.
|
||||
*/
|
||||
public function writeByte(int $byte): void {
|
||||
$this->writeChar(chr($byte & 0xFF));
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the entire contents of this stream to another.
|
||||
*
|
||||
* Will seek to the beginning of the stream if the stream is seekable.
|
||||
*
|
||||
* @param Stream $other Target stream.
|
||||
*/
|
||||
public function copyTo(Stream $other): void {
|
||||
if($this->canSeek())
|
||||
$this->seek(0);
|
||||
|
@ -66,6 +189,9 @@ abstract class Stream implements Stringable, ICloseable {
|
|||
$other->write($this->read(8192));
|
||||
}
|
||||
|
||||
/**
|
||||
* Force write of all buffered output to the stream.
|
||||
*/
|
||||
abstract public function flush(): void;
|
||||
|
||||
abstract public function close(): void;
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
<?php
|
||||
// TempFileStream.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\IO;
|
||||
|
||||
/**
|
||||
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold.
|
||||
*/
|
||||
class TempFileStream extends GenericStream {
|
||||
/**
|
||||
* Default maximum size the stream can be to remain in memory and not be written to disk.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const MAX_MEMORY = 2 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* @param ?string $body Contents of the stream.
|
||||
* @param int $maxMemory Maximum filesize until the file is moved out of memory and to disk.
|
||||
*/
|
||||
public function __construct(?string $body = null, int $maxMemory = self::MAX_MEMORY) {
|
||||
parent::__construct(fopen("php://temp/maxmemory:{$maxMemory}", 'r+b'));
|
||||
|
||||
|
@ -17,6 +29,12 @@ class TempFileStream extends GenericStream {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TempFileStream from a string.
|
||||
*
|
||||
* @param string $string Data to write to the temporary file.
|
||||
* @return TempFileStream Stream representing the given text.
|
||||
*/
|
||||
public static function fromString(string $string): TempFileStream {
|
||||
return new TempFileStream($string);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
<?php
|
||||
// MediaType.php
|
||||
// Created: 2022-02-10
|
||||
// Updated: 2023-07-22
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Implements a structure for representing and comparing media/mime types.
|
||||
*/
|
||||
class MediaType implements Stringable, IComparable, IEquatable {
|
||||
private string $category = '';
|
||||
private string $kind = '';
|
||||
|
|
|
@ -1,17 +1,24 @@
|
|||
<?php
|
||||
// DnsEndPoint.php
|
||||
// Created: 2021-05-02
|
||||
// Updated: 2023-01-01
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
class DnsEndPoint extends EndPoint implements Stringable {
|
||||
/**
|
||||
* A DNS end point.
|
||||
*/
|
||||
class DnsEndPoint extends EndPoint {
|
||||
private string $host;
|
||||
private int $port;
|
||||
|
||||
/**
|
||||
* @param string $host DNS host name.
|
||||
* @param int $port Network port, 0 to leave default.
|
||||
* @throws InvalidArgumentException If $port is less than 0 or greater than 65535.
|
||||
*/
|
||||
public function __construct(string $host, int $port) {
|
||||
if($port < 0 || $port > 0xFFFF)
|
||||
throw new InvalidArgumentException('$port is not a valid port number.');
|
||||
|
@ -19,14 +26,29 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
|||
$this->port = $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves host name.
|
||||
*
|
||||
* @return string Host name.
|
||||
*/
|
||||
public function getHost(): string {
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a port is specified.
|
||||
*
|
||||
* @return bool true if the port number is greater than 0.
|
||||
*/
|
||||
public function hasPort(): bool {
|
||||
return $this->port > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves port number.
|
||||
*
|
||||
* @return int Network port number.
|
||||
*/
|
||||
public function getPort(): int {
|
||||
return $this->port;
|
||||
}
|
||||
|
@ -34,6 +56,7 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
|||
public function __serialize(): array {
|
||||
return [$this->host, $this->port];
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->host = $serialized[0];
|
||||
$this->port = $serialized[1];
|
||||
|
@ -45,11 +68,18 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
|||
&& $this->host === $other->host;
|
||||
}
|
||||
|
||||
public static function parse(string $string): EndPoint {
|
||||
/**
|
||||
* Attempts to parse a string into a DNS end point.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @throws InvalidArgumentException If $string does not contain a valid DNS end point string.
|
||||
* @return DnsEndPoint Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): DnsEndPoint {
|
||||
$parts = explode(':', $string);
|
||||
if(count($parts) < 2)
|
||||
throw new InvalidArgumentException('$string is not a valid host and port combo.');
|
||||
return new DnsEndPoint($parts[0], (int)$parts[1]);
|
||||
return new DnsEndPoint($parts[0], (int)($parts[1] ?? '0'));
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// EndPoint.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2022-02-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -10,6 +10,9 @@ use JsonSerializable;
|
|||
use Stringable;
|
||||
use Index\IEquatable;
|
||||
|
||||
/**
|
||||
* Represents a generic network end point.
|
||||
*/
|
||||
abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
|
||||
abstract public function equals(mixed $other): bool;
|
||||
abstract public function __toString(): string;
|
||||
|
@ -18,29 +21,36 @@ abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
|
|||
return (string)$this;
|
||||
}
|
||||
|
||||
public static function parse(string $endPoint): EndPoint {
|
||||
if($endPoint === '')
|
||||
throw new InvalidArgumentException('$endPoint is not a valid endpoint string.');
|
||||
/**
|
||||
* Attempts to parse a string into a known end point object.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @throws InvalidArgumentException If $string does not contain a valid end point string.
|
||||
* @return EndPoint Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): EndPoint {
|
||||
if($string === '')
|
||||
throw new InvalidArgumentException('$string is not a valid endpoint string.');
|
||||
|
||||
$firstChar = $endPoint[0];
|
||||
$firstChar = $string[0];
|
||||
|
||||
if($firstChar === '/' || str_starts_with($endPoint, 'unix:'))
|
||||
$endPoint = UnixEndPoint::parse($endPoint);
|
||||
if($firstChar === '/' || str_starts_with($string, 'unix:'))
|
||||
$endPoint = UnixEndPoint::parse($string);
|
||||
elseif($firstChar === '[') { // IPv6
|
||||
if(str_contains($endPoint, ']:'))
|
||||
$endPoint = IPEndPoint::parse($endPoint);
|
||||
if(str_contains($string, ']:'))
|
||||
$endPoint = IPEndPoint::parse($string);
|
||||
else
|
||||
$endPoint = new IPEndPoint(IPAddress::parse(trim($endPoint, '[]')), 0);
|
||||
$endPoint = new IPEndPoint(IPAddress::parse(trim($string, '[]')), 0);
|
||||
} elseif(is_numeric($firstChar)) { // IPv4
|
||||
if(str_contains($endPoint, ':'))
|
||||
$endPoint = IPEndPoint::parse($endPoint);
|
||||
if(str_contains($string, ':'))
|
||||
$endPoint = IPEndPoint::parse($string);
|
||||
else
|
||||
$endPoint = new IPEndPoint(IPAddress::parse($endPoint), 0);
|
||||
$endPoint = new IPEndPoint(IPAddress::parse($string), 0);
|
||||
} else { // DNS
|
||||
if(str_contains($endPoint, ':'))
|
||||
$endPoint = DnsEndPoint::parse($endPoint);
|
||||
if(str_contains($string, ':'))
|
||||
$endPoint = DnsEndPoint::parse($string);
|
||||
else
|
||||
$endPoint = new DnsEndPoint($endPoint, 0);
|
||||
$endPoint = new DnsEndPoint($string, 0);
|
||||
}
|
||||
|
||||
return $endPoint;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IPAddress.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -10,33 +10,89 @@ use JsonSerializable;
|
|||
use Stringable;
|
||||
use Index\IEquatable;
|
||||
|
||||
/**
|
||||
* Represents an IP address.
|
||||
*/
|
||||
final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
||||
/**
|
||||
* Unknown IP version.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const UNKNOWN = 0;
|
||||
|
||||
/**
|
||||
* IPv4
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const V4 = 4;
|
||||
|
||||
/**
|
||||
* IPv6
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public const V6 = 6;
|
||||
|
||||
private string $raw;
|
||||
private int $width;
|
||||
|
||||
/**
|
||||
* @param string $raw Raw IP address data.
|
||||
*/
|
||||
public function __construct(string $raw) {
|
||||
$this->raw = $raw;
|
||||
$this->width = strlen($raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get raw address bytes.
|
||||
*
|
||||
* @return string Raw address bytes.
|
||||
*/
|
||||
public function getRaw(): string {
|
||||
return $this->raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the width of the raw address byte data.
|
||||
*
|
||||
* 4 bytes for IPv4
|
||||
* 16 bytes for IPv6
|
||||
*
|
||||
* @return int Raw address byte width.
|
||||
*/
|
||||
public function getWidth(): int {
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets safe string representation of IP address.
|
||||
*
|
||||
* IPv6 addresses will be wrapped in square brackets.
|
||||
* Use getCleanAddress if this is undesirable.
|
||||
*
|
||||
* @return string Safe string IP address.
|
||||
*/
|
||||
public function getAddress(): string {
|
||||
return sprintf($this->isV6() ? '[%s]' : '%s', inet_ntop($this->raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets string representation of IP address.
|
||||
*
|
||||
* @return string String IP address.
|
||||
*/
|
||||
public function getCleanAddress(): string {
|
||||
return inet_ntop($this->raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets IP version this address is for.
|
||||
*
|
||||
* @return int IP version.
|
||||
*/
|
||||
public function getVersion(): int {
|
||||
if($this->isV4())
|
||||
return self::V4;
|
||||
|
@ -45,9 +101,20 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
return self::UNKNOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is an IPv4 address.
|
||||
*
|
||||
* @return bool true if IPv4.
|
||||
*/
|
||||
public function isV4(): bool {
|
||||
return $this->width === 4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether this is an IPv6 address.
|
||||
*
|
||||
* @return bool true if IPv6.
|
||||
*/
|
||||
public function isV6(): bool {
|
||||
return $this->width === 16;
|
||||
}
|
||||
|
@ -60,6 +127,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
return $other instanceof IPAddress && $this->getRaw() === $other->getRaw();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function jsonSerialize(): mixed {
|
||||
return inet_ntop($this->raw);
|
||||
}
|
||||
|
@ -67,15 +135,23 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
|||
public function __serialize(): array {
|
||||
return [$this->raw];
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->raw = $serialized[0];
|
||||
$this->width = strlen($this->raw);
|
||||
}
|
||||
|
||||
public static function parse(string $address): self {
|
||||
$parsed = inet_pton($address);
|
||||
/**
|
||||
* Attempts to parse a string into an IP address.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @throws InvalidArgumentException If $string does not contain a valid IP address string.
|
||||
* @return IPAddress Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): self {
|
||||
$parsed = inet_pton($string);
|
||||
if($parsed === false)
|
||||
throw new InvalidArgumentException('$address is not a valid IP address.');
|
||||
throw new InvalidArgumentException('$string is not a valid IP address.');
|
||||
return new static($parsed);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// IPAddressRange.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2023-01-01
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
|
@ -10,23 +10,45 @@ use JsonSerializable;
|
|||
use Stringable;
|
||||
use Index\IEquatable;
|
||||
|
||||
/**
|
||||
* Represents a CIDR range of IP addresses.
|
||||
*/
|
||||
final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
||||
private IPAddress $base;
|
||||
private int $mask;
|
||||
|
||||
/**
|
||||
* @param IPAddress $base Base IP address.
|
||||
* @param int $mask CIDR mask.
|
||||
*/
|
||||
public function __construct(IPAddress $base, int $mask) {
|
||||
$this->base = $base;
|
||||
$this->mask = $mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the base IP address.
|
||||
*
|
||||
* @return IPAddress Base IP address.
|
||||
*/
|
||||
public function getBaseAddress(): IPAddress {
|
||||
return $this->base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the CIDR mask.
|
||||
*
|
||||
* @return int CIDR mask.
|
||||
*/
|
||||
public function getMask(): int {
|
||||
return $this->mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves full CIDR representation of this range.
|
||||
*
|
||||
* @return string CIDR string.
|
||||
*/
|
||||
public function getCIDR(): string {
|
||||
return ((string)$this->base) . '/' . $this->getMask();
|
||||
}
|
||||
|
@ -41,6 +63,12 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
|||
&& $this->base->equals($other->base);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given IP address matches with this range.
|
||||
*
|
||||
* @param IPAddress $address IP address to check.
|
||||
* @return bool true if the address matches.
|
||||
*/
|
||||
public function match(IPAddress $address): bool {
|
||||
$width = $this->base->getWidth();
|
||||
if($address->getWidth() !== $width)
|
||||
|
@ -83,15 +111,23 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
|||
'm' => $this->mask,
|
||||
];
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->base = new IPAddress($serialized['b']);
|
||||
$this->mask = $serialized['m'];
|
||||
}
|
||||
|
||||
public static function parse(string $cidr): IPAddressRange {
|
||||
$parts = explode('/', $cidr, 2);
|
||||
/**
|
||||
* Attempts to parse a string into an IP address range.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @throws InvalidArgumentException If $string does not contain a valid CIDR range string.
|
||||
* @return IPAddressRange Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): IPAddressRange {
|
||||
$parts = explode('/', $string, 2);
|
||||
if(empty($parts[0]) || empty($parts[1]))
|
||||
throw new InvalidArgumentException('$cidr is not a valid CIDR range.');
|
||||
throw new InvalidArgumentException('$string is not a valid CIDR range.');
|
||||
return new static(IPAddress::parse($parts[0]), (int)$parts[1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,25 @@
|
|||
<?php
|
||||
// IPEndPoint.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2023-01-01
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\XString;
|
||||
|
||||
/**
|
||||
* Represents an IP address end point.
|
||||
*/
|
||||
class IPEndPoint extends EndPoint {
|
||||
private IPAddress $address;
|
||||
private int $port;
|
||||
|
||||
/**
|
||||
* @param IPAddress $address IP address.
|
||||
* @param int $port Network port, 0 to leave default.
|
||||
* @throws InvalidArgumentException If $port is less than 0 or greater than 65535.
|
||||
*/
|
||||
public function __construct(IPAddress $address, int $port) {
|
||||
if($port < 0 || $port > 0xFFFF)
|
||||
throw new InvalidArgumentException('$port is not a valid port number.');
|
||||
|
@ -19,14 +27,29 @@ class IPEndPoint extends EndPoint {
|
|||
$this->port = $port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the IP address.
|
||||
*
|
||||
* @return IPAddress IP address.
|
||||
*/
|
||||
public function getAddress(): IPAddress {
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a port is specified.
|
||||
*
|
||||
* @return bool true if the port number is greater than 0.
|
||||
*/
|
||||
public function hasPort(): bool {
|
||||
return $this->port > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves port number.
|
||||
*
|
||||
* @return int Network port number.
|
||||
*/
|
||||
public function getPort(): int {
|
||||
return $this->port;
|
||||
}
|
||||
|
@ -34,6 +57,7 @@ class IPEndPoint extends EndPoint {
|
|||
public function __serialize(): array {
|
||||
return [$this->address, $this->port];
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->address = $serialized[0];
|
||||
$this->port = $serialized[1];
|
||||
|
@ -45,7 +69,14 @@ class IPEndPoint extends EndPoint {
|
|||
&& $this->address->equals($other->address);
|
||||
}
|
||||
|
||||
public static function parse(string $string): EndPoint {
|
||||
/**
|
||||
* Attempts to parse a string into an IP end point.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @throws InvalidArgumentException If $string does not contain a valid IP end point string.
|
||||
* @return IPEndPoint Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): IPEndPoint {
|
||||
if(str_starts_with($string, '[')) { // IPv6
|
||||
$closing = strpos($string, ']');
|
||||
if($closing === false)
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
<?php
|
||||
// UnixEndPoint.php
|
||||
// Created: 2021-04-30
|
||||
// Updated: 2022-02-27
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Net;
|
||||
|
||||
/**
|
||||
* Represents a UNIX socket path end point.
|
||||
*/
|
||||
class UnixEndPoint extends EndPoint {
|
||||
private string $path;
|
||||
|
||||
/**
|
||||
* @param string $socketPath Path to the socket file.
|
||||
*/
|
||||
public function __construct(string $socketPath) {
|
||||
$this->path = $socketPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets path to the socket file.
|
||||
*
|
||||
* @return string Path to the socket file.
|
||||
*/
|
||||
public function getPath(): string {
|
||||
return $this->path;
|
||||
}
|
||||
|
@ -19,6 +30,7 @@ class UnixEndPoint extends EndPoint {
|
|||
public function __serialize(): array {
|
||||
return [$this->path];
|
||||
}
|
||||
|
||||
public function __unserialize(array $serialized): void {
|
||||
$this->path = $serialized[0];
|
||||
}
|
||||
|
@ -28,13 +40,19 @@ class UnixEndPoint extends EndPoint {
|
|||
&& $this->path === $other->path;
|
||||
}
|
||||
|
||||
public static function parse(string $socketPath): UnixEndPoint {
|
||||
if(str_starts_with($socketPath, 'unix:')) {
|
||||
$uri = parse_url($socketPath);
|
||||
$socketPath = $uri['path'] ?? '';
|
||||
/**
|
||||
* Attempts to parse a string into an UNIX end point.
|
||||
*
|
||||
* @param string $string String to parse.
|
||||
* @return UnixEndPoint Representation of the given string.
|
||||
*/
|
||||
public static function parse(string $string): UnixEndPoint {
|
||||
if(str_starts_with($string, 'unix:')) {
|
||||
$uri = parse_url($string);
|
||||
$string = $uri['path'] ?? '';
|
||||
}
|
||||
|
||||
return new UnixEndPoint($socketPath);
|
||||
return new UnixEndPoint($string);
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
|
|
|
@ -1,19 +1,38 @@
|
|||
<?php
|
||||
// PerformanceCounter.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2022-02-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
/**
|
||||
* Represents a performance counter.
|
||||
*/
|
||||
final class PerformanceCounter {
|
||||
/**
|
||||
* Timer frequency in ticks per second.
|
||||
*
|
||||
* @return int|float Timer frequency.
|
||||
*/
|
||||
public static function getFrequency(): int|float {
|
||||
return 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current ticks from the system.
|
||||
*
|
||||
* @return int|float Ticks count.
|
||||
*/
|
||||
public static function getTicks(): int|float {
|
||||
return hrtime(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks elapsed since the given value.
|
||||
*
|
||||
* @param int|float $since Starting ticks value.
|
||||
* @return int|float Ticks count since.
|
||||
*/
|
||||
public static function getTicksSince(int|float $since): int|float {
|
||||
return self::getTicks() - $since;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// Stopwatch.php
|
||||
// Created: 2021-04-26
|
||||
// Updated: 2022-02-16
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
|
@ -17,11 +17,17 @@ class Stopwatch {
|
|||
$this->frequency = PerformanceCounter::getFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start time measurement.
|
||||
*/
|
||||
public function start(): void {
|
||||
if($this->start < 0)
|
||||
$this->start = PerformanceCounter::getTicks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop time measurement.
|
||||
*/
|
||||
public function stop(): void {
|
||||
if($this->start < 0)
|
||||
return;
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
<?php
|
||||
// TimingPoint.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2022-02-28
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
/**
|
||||
* Represents a timing point.
|
||||
*/
|
||||
class TimingPoint {
|
||||
private int|float $timePoint;
|
||||
private int|float $duration;
|
||||
private string $name;
|
||||
private string $comment;
|
||||
|
||||
/**
|
||||
* @param int|float $timePoint Starting tick count.
|
||||
* @param int|float $duration Duration ticks count.
|
||||
* @param string $name Name of the timing point.
|
||||
* @param string $comment Timing point comment.
|
||||
*/
|
||||
public function __construct(
|
||||
int|float $timePoint,
|
||||
int|float $duration,
|
||||
|
@ -23,26 +32,56 @@ class TimingPoint {
|
|||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the starting ticks count.
|
||||
*
|
||||
* @return int|float Starting ticks count.
|
||||
*/
|
||||
public function getTimePointTicks(): int|float {
|
||||
return $this->timePoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration ticks count.
|
||||
*
|
||||
* @return int|float Duration ticks count.
|
||||
*/
|
||||
public function getDurationTicks(): int|float {
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration time amount.
|
||||
*
|
||||
* @return float Duration time amount.
|
||||
*/
|
||||
public function getDurationTime(): float {
|
||||
return $this->duration / PerformanceCounter::getFrequency();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets time point name.
|
||||
*
|
||||
* @return string Time point name.
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether time point has a comment.
|
||||
*
|
||||
* @return bool true if the time point has a comment.
|
||||
*/
|
||||
public function hasComment(): bool {
|
||||
return $this->comment !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets time point comment.
|
||||
*
|
||||
* @return string Time point comment.
|
||||
*/
|
||||
public function getComment(): string {
|
||||
return $this->comment;
|
||||
}
|
||||
|
|
|
@ -1,45 +1,79 @@
|
|||
<?php
|
||||
// Timings.php
|
||||
// Created: 2022-02-16
|
||||
// Updated: 2022-02-16
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index\Performance;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Represents a Stopwatch with timing points.
|
||||
*/
|
||||
class Timings {
|
||||
private Stopwatch $sw;
|
||||
private int|float $lastLapTicks;
|
||||
private array $laps;
|
||||
|
||||
/**
|
||||
* @param ?Stopwatch $sw Stopwatch to measure timing points from, null to start a new one.
|
||||
*/
|
||||
public function __construct(?Stopwatch $sw = null) {
|
||||
$this->sw = $sw ?? Stopwatch::startNew();
|
||||
$this->lastLapTicks = $this->sw->getElapsedTicks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying stopwatch object.
|
||||
*
|
||||
* @return Stopwatch Stopwatch object.
|
||||
*/
|
||||
public function getStopwatch(): Stopwatch {
|
||||
return $this->sw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns timing points.
|
||||
*
|
||||
* @return TimingPoint[] Timing points.
|
||||
*/
|
||||
public function getLaps(): array {
|
||||
return $this->laps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the underlying stopwatch.
|
||||
*/
|
||||
public function start(): void {
|
||||
$this->sw->start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the underlying stopwatch.
|
||||
*/
|
||||
public function stop(): void {
|
||||
$this->sw->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the underlying stopwatch is running.
|
||||
*
|
||||
* @return bool true if it is running.
|
||||
*/
|
||||
public function isRunning(): bool {
|
||||
return $this->sw->isRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a timing point.
|
||||
*
|
||||
* @param string $name Timing point name, must be alphanumeric.
|
||||
* @param string $comment Timing point comment.
|
||||
* @throws InvalidArgumentException If $name is not alphanumeric.
|
||||
*/
|
||||
public function lap(string $name, string $comment = ''): void {
|
||||
if(!ctype_alnum($name))
|
||||
throw new InvalidArgumentException('$name must be alpha-numeric.');
|
||||
throw new InvalidArgumentException('$name must be alphanumeric.');
|
||||
|
||||
$lapTicks = $this->sw->getElapsedTicks();
|
||||
$elapsed = $lapTicks - $this->lastLapTicks;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// XDateTime.php
|
||||
// Created: 2024-07-31
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
namespace Index;
|
||||
|
||||
|
@ -9,6 +9,9 @@ use DateTimeImmutable;
|
|||
use DateTimeInterface;
|
||||
use DateTimeZone;
|
||||
|
||||
/**
|
||||
* Provides a set of DateTime related utilities.
|
||||
*/
|
||||
final class XDateTime {
|
||||
private const COMPARE_FORMAT = 'YmdHisu';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?php
|
||||
// BencodeTest.php
|
||||
// Created: 2023-07-21
|
||||
// Updated: 2024-07-31
|
||||
// Updated: 2024-08-01
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
@ -29,7 +29,7 @@ final class BencodeTest extends TestCase {
|
|||
$this->assertArrayHasKey('private', $decoded['info']);
|
||||
$this->assertEquals(1, $decoded['info']['private']);
|
||||
|
||||
$decoded = Bencode::decode(base64_decode(self::TORRENT), dictAsObject: true);
|
||||
$decoded = Bencode::decode(base64_decode(self::TORRENT), associative: false);
|
||||
|
||||
$this->assertEquals('https://tracker.flashii.net/announce.php/meow', $decoded->announce);
|
||||
$this->assertEquals(1689973664, $decoded->{'creation date'});
|
||||
|
|
Loading…
Reference in a new issue