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
|
<?php
|
||||||
// Bencode.php
|
// Bencode.php
|
||||||
// Created: 2022-01-13
|
// Created: 2022-01-13
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Bencode;
|
namespace Index\Bencode;
|
||||||
|
|
||||||
|
@ -15,44 +15,57 @@ use Index\IO\TempFileStream;
|
||||||
* Provides a Bencode serialiser.
|
* Provides a Bencode serialiser.
|
||||||
*/
|
*/
|
||||||
final class Bencode {
|
final class Bencode {
|
||||||
|
/**
|
||||||
|
* Default maximum depth.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const DEFAULT_DEPTH = 512;
|
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)
|
if($depth < 1)
|
||||||
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
throw new RuntimeException('Maximum depth reached, structure is too dense.');
|
||||||
|
|
||||||
switch(gettype($input)) {
|
switch(gettype($value)) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return sprintf('%d:%s', strlen($input), $input);
|
return sprintf('%d:%s', strlen($value), $value);
|
||||||
|
|
||||||
case 'integer':
|
case 'integer':
|
||||||
return sprintf('i%de', $input);
|
return sprintf('i%de', $value);
|
||||||
|
|
||||||
case 'array':
|
case 'array':
|
||||||
if(array_is_list($input)) {
|
if(array_is_list($value)) {
|
||||||
$output = 'l';
|
$output = 'l';
|
||||||
foreach($input as $item)
|
foreach($value as $item)
|
||||||
$output .= self::encode($item, $depth - 1);
|
$output .= self::encode($item, $depth - 1);
|
||||||
} else {
|
} else {
|
||||||
$output = 'd';
|
$output = 'd';
|
||||||
foreach($input as $key => $value) {
|
foreach($value as $_key => $_value) {
|
||||||
$output .= self::encode((string)$key, $depth - 1);
|
$output .= self::encode((string)$_key, $depth - 1);
|
||||||
$output .= self::encode($value, $depth - 1);
|
$output .= self::encode($_value, $depth - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $output . 'e';
|
return $output . 'e';
|
||||||
|
|
||||||
case 'object':
|
case 'object':
|
||||||
if($input instanceof IBencodeSerialisable)
|
if($value instanceof IBencodeSerialisable)
|
||||||
return self::encode($input->bencodeSerialise(), $depth - 1);
|
return self::encode($value->bencodeSerialise(), $depth - 1);
|
||||||
|
|
||||||
$input = get_object_vars($input);
|
$value = get_object_vars($value);
|
||||||
$output = 'd';
|
$output = 'd';
|
||||||
|
|
||||||
foreach($input as $key => $value) {
|
foreach($value as $_key => $_value) {
|
||||||
$output .= self::encode((string)$key, $depth - 1);
|
$output .= self::encode((string)$_key, $depth - 1);
|
||||||
$output .= self::encode($value, $depth - 1);
|
$output .= self::encode($_value, $depth - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $output . 'e';
|
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)) {
|
* Decodes a bencoded string.
|
||||||
$input = TempFileStream::fromString($input);
|
*
|
||||||
$input->seek(0);
|
* @param mixed $bencoded The bencoded string being decoded. Can be an Index Stream, readable resource or a string.
|
||||||
} elseif(is_resource($input))
|
* @param int $depth Maximum nesting depth of the structure being decoded. The value must be greater than 0.
|
||||||
$input = new GenericStream($input);
|
* @param bool $associative When true, Bencoded dictionaries will be returned as associative arrays; when false, Bencoded dictionaries will be returned as objects.
|
||||||
elseif(!($input instanceof Stream))
|
* @throws InvalidArgumentException If $bencoded string is not set to a valid type.
|
||||||
throw new InvalidArgumentException('$input must be a string, an Index Stream or a file resource.');
|
* @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)
|
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)
|
if($char === null)
|
||||||
throw new RuntimeException('Unexpected end of stream in $input.');
|
throw new RuntimeException('Unexpected end of stream in $bencoded.');
|
||||||
|
|
||||||
switch($char) {
|
switch($char) {
|
||||||
case 'i':
|
case 'i':
|
||||||
$number = '';
|
$number = '';
|
||||||
|
|
||||||
for(;;) {
|
for(;;) {
|
||||||
$char = $input->readChar();
|
$char = $bencoded->readChar();
|
||||||
if($char === null)
|
if($char === null)
|
||||||
throw new RuntimeException('Unexpected end of stream while parsing integer.');
|
throw new RuntimeException('Unexpected end of stream while parsing integer.');
|
||||||
if($char === 'e')
|
if($char === 'e')
|
||||||
|
@ -105,13 +129,13 @@ final class Bencode {
|
||||||
$list = [];
|
$list = [];
|
||||||
|
|
||||||
for(;;) {
|
for(;;) {
|
||||||
$char = $input->readChar();
|
$char = $bencoded->readChar();
|
||||||
if($char === null)
|
if($char === null)
|
||||||
throw new RuntimeException('Unexpected end of stream while parsing list.');
|
throw new RuntimeException('Unexpected end of stream while parsing list.');
|
||||||
if($char === 'e')
|
if($char === 'e')
|
||||||
break;
|
break;
|
||||||
$input->seek(-1, Stream::CURRENT);
|
$bencoded->seek(-1, Stream::CURRENT);
|
||||||
$list[] = self::decode($input, $depth - 1, $dictAsObject);
|
$list[] = self::decode($bencoded, $depth - 1, $associative);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $list;
|
return $list;
|
||||||
|
@ -120,18 +144,18 @@ final class Bencode {
|
||||||
$dict = [];
|
$dict = [];
|
||||||
|
|
||||||
for(;;) {
|
for(;;) {
|
||||||
$char = $input->readChar();
|
$char = $bencoded->readChar();
|
||||||
if($char === null)
|
if($char === null)
|
||||||
throw new RuntimeException('Unexpected end of stream while parsing dictionary');
|
throw new RuntimeException('Unexpected end of stream while parsing dictionary');
|
||||||
if($char === 'e')
|
if($char === 'e')
|
||||||
break;
|
break;
|
||||||
if(!ctype_digit($char))
|
if(!ctype_digit($char))
|
||||||
throw new RuntimeException('Unexpected dictionary key type, expected a string.');
|
throw new RuntimeException('Unexpected dictionary key type, expected a string.');
|
||||||
$input->seek(-1, Stream::CURRENT);
|
$bencoded->seek(-1, Stream::CURRENT);
|
||||||
$dict[self::decode($input, $depth - 1, $dictAsObject)] = self::decode($input, $depth - 1, $dictAsObject);
|
$dict[self::decode($bencoded, $depth - 1, $associative)] = self::decode($bencoded, $depth - 1, $associative);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($dictAsObject)
|
if(!$associative)
|
||||||
$dict = (object)$dict;
|
$dict = (object)$dict;
|
||||||
|
|
||||||
return $dict;
|
return $dict;
|
||||||
|
@ -143,7 +167,7 @@ final class Bencode {
|
||||||
$length = $char;
|
$length = $char;
|
||||||
|
|
||||||
for(;;) {
|
for(;;) {
|
||||||
$char = $input->readChar();
|
$char = $bencoded->readChar();
|
||||||
if($char === null)
|
if($char === null)
|
||||||
throw new RuntimeException('Unexpected end of character while parsing string length.');
|
throw new RuntimeException('Unexpected end of character while parsing string length.');
|
||||||
if($char === ':')
|
if($char === ':')
|
||||||
|
@ -156,7 +180,7 @@ final class Bencode {
|
||||||
$length .= $char;
|
$length .= $char;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $input->read((int)$length);
|
return $bencoded->read((int)$length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
// ByteFormat.php
|
// ByteFormat.php
|
||||||
// Created: 2023-07-05
|
// Created: 2023-07-05
|
||||||
// Updated: 2023-07-05
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index;
|
namespace Index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a byte formatter for file sizes.
|
||||||
|
*/
|
||||||
final class ByteFormat {
|
final class ByteFormat {
|
||||||
/**
|
/**
|
||||||
* Whether the default behaviour for the format function is decimal (power of 10) or not (power of 2).
|
* Whether the default behaviour for the format function is decimal (power of 10) or not (power of 2).
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
// DataException.php
|
// DataException.php
|
||||||
// Created: 2021-05-02
|
// Created: 2021-05-02
|
||||||
// Updated: 2021-05-12
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data;
|
namespace Index\Data;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception type of the Index\Data namespace.
|
* Exception type for the Index\Data namespace.
|
||||||
*/
|
*/
|
||||||
class DataException extends RuntimeException {}
|
class DataException extends RuntimeException {}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// DbStatementCache.php
|
// DbStatementCache.php
|
||||||
// Created: 2023-07-21
|
// Created: 2023-07-21
|
||||||
// Updated: 2023-07-21
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data;
|
namespace Index\Data;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ class DbStatementCache {
|
||||||
private IDbConnection $dbConn;
|
private IDbConnection $dbConn;
|
||||||
private array $stmts = [];
|
private array $stmts = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IDbConnection $dbConn Connection to use with this cache.
|
||||||
|
*/
|
||||||
public function __construct(IDbConnection $dbConn) {
|
public function __construct(IDbConnection $dbConn) {
|
||||||
$this->dbConn = $dbConn;
|
$this->dbConn = $dbConn;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +23,12 @@ class DbStatementCache {
|
||||||
return hash('xxh3', $query, true);
|
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 {
|
public function get(string $query): IDbStatement {
|
||||||
$hash = self::hash($query);
|
$hash = self::hash($query);
|
||||||
|
|
||||||
|
@ -32,10 +41,18 @@ class DbStatementCache {
|
||||||
return $this->stmts[$hash] = $this->dbConn->prepare($query);
|
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 {
|
public function remove(string $query): void {
|
||||||
unset($this->stmts[self::hash($query)]);
|
unset($this->stmts[self::hash($query)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes all statement instances and resets the cache.
|
||||||
|
*/
|
||||||
public function clear(): void {
|
public function clear(): void {
|
||||||
foreach($this->stmts as $stmt)
|
foreach($this->stmts as $stmt)
|
||||||
$stmt->close();
|
$stmt->close();
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// DbMigrationManager.php
|
// DbMigrationManager.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
|
@ -15,7 +15,15 @@ use Index\Data\IDbStatement;
|
||||||
use Index\Data\DbType;
|
use Index\Data\DbType;
|
||||||
use Index\Data\SQLite\SQLiteConnection;
|
use Index\Data\SQLite\SQLiteConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a common interface for database migrations.
|
||||||
|
*/
|
||||||
class DbMigrationManager {
|
class DbMigrationManager {
|
||||||
|
/**
|
||||||
|
* Default table name for migrations.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
public const DEFAULT_TABLE = 'ndx_migrations';
|
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);';
|
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 $checkStmt;
|
||||||
private IDbStatement $insertStmt;
|
private IDbStatement $insertStmt;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IDbConnection $conn Connection to apply to migrations to.
|
||||||
|
* @param string $tableName Name of the migration tracking table.
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private IDbConnection $conn,
|
private IDbConnection $conn,
|
||||||
private string $tableName = self::DEFAULT_TABLE,
|
private string $tableName = self::DEFAULT_TABLE,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the migration tracking table if it doesn't exist.
|
||||||
|
*/
|
||||||
public function createTrackingTable(): void {
|
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)';
|
$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_TABLE, $this->tableName, $nameType));
|
||||||
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
|
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the migration tracking table.
|
||||||
|
*/
|
||||||
public function destroyTrackingTable(): void {
|
public function destroyTrackingTable(): void {
|
||||||
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
|
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares statements required for migrations.
|
||||||
|
*/
|
||||||
public function prepareStatements(): void {
|
public function prepareStatements(): void {
|
||||||
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
|
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
|
||||||
$this->insertStmt = $this->conn->prepare(sprintf(self::INSERT_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 {
|
public function init(): void {
|
||||||
$this->createTrackingTable();
|
$this->createTrackingTable();
|
||||||
$this->prepareStatements();
|
$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 {
|
public function checkMigration(string $name): bool {
|
||||||
$this->checkStmt->reset();
|
$this->checkStmt->reset();
|
||||||
$this->checkStmt->addParameter(1, $name, DbType::STRING);
|
$this->checkStmt->addParameter(1, $name, DbType::STRING);
|
||||||
|
@ -76,6 +106,12 @@ EOF;
|
||||||
return $result->next() && !$result->isNull(0);
|
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 {
|
public function completeMigration(string $name, ?DateTimeInterface $dateTime = null): void {
|
||||||
$dateTime = XDateTime::toISO8601String($dateTime);
|
$dateTime = XDateTime::toISO8601String($dateTime);
|
||||||
|
|
||||||
|
@ -85,10 +121,25 @@ EOF;
|
||||||
$this->insertStmt->execute();
|
$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 {
|
public function template(string $name): string {
|
||||||
return sprintf(self::TEMPLATE, $name);
|
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 {
|
public function createFileName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||||
if(empty($name))
|
if(empty($name))
|
||||||
throw new InvalidArgumentException('$name may not be empty.');
|
throw new InvalidArgumentException('$name may not be empty.');
|
||||||
|
@ -102,6 +153,15 @@ EOF;
|
||||||
return $dateTime->format('Y_m_d_His_') . trim($name, '_');
|
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 {
|
public function createClassName(string $name, ?DateTimeInterface $dateTime = null): string {
|
||||||
if(empty($name))
|
if(empty($name))
|
||||||
throw new InvalidArgumentException('$name may not be empty.');
|
throw new InvalidArgumentException('$name may not be empty.');
|
||||||
|
@ -121,6 +181,13 @@ EOF;
|
||||||
return $name . $dateTime->format('_Ymd_His');
|
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 {
|
public function createNames(string $baseName, ?DateTimeInterface $dateTime = null): object {
|
||||||
$dateTime ??= new DateTimeImmutable('now');
|
$dateTime ??= new DateTimeImmutable('now');
|
||||||
|
|
||||||
|
@ -131,6 +198,12 @@ EOF;
|
||||||
return $names;
|
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 {
|
public function processMigrations(IDbMigrationRepo $migrations): array {
|
||||||
$migrations = $migrations->getMigrations();
|
$migrations = $migrations->getMigrations();
|
||||||
$completed = [];
|
$completed = [];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// FsDbMigrationInfo.php
|
// FsDbMigrationInfo.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ class FsDbMigrationInfo implements IDbMigrationInfo {
|
||||||
private string $name;
|
private string $name;
|
||||||
private string $className;
|
private string $className;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $path Filesystem path to the migration file.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
$this->name = $name = pathinfo($path, PATHINFO_FILENAME);
|
$this->name = $name = pathinfo($path, PATHINFO_FILENAME);
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
// FsDbMigrationRepo.php
|
// FsDbMigrationRepo.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2023-01-07
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
class FsDbMigrationRepo implements IDbMigrationRepo {
|
class FsDbMigrationRepo implements IDbMigrationRepo {
|
||||||
|
/**
|
||||||
|
* @param string $path Filesystem path to the directory containing the migration files.
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $path
|
private string $path
|
||||||
) {}
|
) {}
|
||||||
|
@ -22,6 +25,13 @@ class FsDbMigrationRepo implements IDbMigrationRepo {
|
||||||
return $migrations;
|
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 {
|
public function saveMigrationTemplate(string $name, string $body): void {
|
||||||
if(!is_dir($this->path))
|
if(!is_dir($this->path))
|
||||||
mkdir($this->path, 0777, true);
|
mkdir($this->path, 0777, true);
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
<?php
|
<?php
|
||||||
// IDbMigration.php
|
// IDbMigration.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2023-01-07
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for migration classes to inherit.
|
||||||
|
*/
|
||||||
interface IDbMigration {
|
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;
|
public function migrate(IDbConnection $conn): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,34 @@
|
||||||
<?php
|
<?php
|
||||||
// IDbMigrationInfo.php
|
// IDbMigrationInfo.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2023-01-07
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
use Index\Data\IDbConnection;
|
use Index\Data\IDbConnection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information on a migration repository item.
|
||||||
|
*/
|
||||||
interface IDbMigrationInfo {
|
interface IDbMigrationInfo {
|
||||||
|
/**
|
||||||
|
* Returns the name of the migration.
|
||||||
|
*
|
||||||
|
* @return string Migration name.
|
||||||
|
*/
|
||||||
public function getName(): string;
|
public function getName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the class name of the migration.
|
||||||
|
*
|
||||||
|
* @return string Migration class name.
|
||||||
|
*/
|
||||||
public function getClassName(): string;
|
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;
|
public function migrate(IDbConnection $conn): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
<?php
|
<?php
|
||||||
// IDbMigrationRepo.php
|
// IDbMigrationRepo.php
|
||||||
// Created: 2023-01-07
|
// Created: 2023-01-07
|
||||||
// Updated: 2023-01-07
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Data\Migration;
|
namespace Index\Data\Migration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a repository of migrations.
|
||||||
|
*/
|
||||||
interface IDbMigrationRepo {
|
interface IDbMigrationRepo {
|
||||||
|
/**
|
||||||
|
* Returns info on migrations contained in this repository.
|
||||||
|
*
|
||||||
|
* @return IDbMigrationInfo[] Collection of migration infos.
|
||||||
|
*/
|
||||||
public function getMigrations(): array;
|
public function getMigrations(): array;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,33 @@
|
||||||
<?php
|
<?php
|
||||||
// BencodedContent.php
|
// BencodedContent.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
use Index\Bencode\Bencode;
|
use Index\Bencode\Bencode;
|
||||||
use Index\Bencode\IBencodeSerialisable;
|
use Index\Bencode\IBencodeSerialisable;
|
||||||
use Index\IO\Stream;
|
use Index\IO\Stream;
|
||||||
use Index\IO\FileStream;
|
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;
|
private mixed $content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $content Content to be bencoded.
|
||||||
|
*/
|
||||||
public function __construct(mixed $content) {
|
public function __construct(mixed $content) {
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves unencoded content.
|
||||||
|
*
|
||||||
|
* @return mixed Content.
|
||||||
|
*/
|
||||||
public function getContent(): mixed {
|
public function getContent(): mixed {
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
@ -26,6 +36,11 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the content.
|
||||||
|
*
|
||||||
|
* @return string Bencoded string.
|
||||||
|
*/
|
||||||
public function encode(): string {
|
public function encode(): string {
|
||||||
return Bencode::encode($this->content);
|
return Bencode::encode($this->content);
|
||||||
}
|
}
|
||||||
|
@ -34,14 +49,31 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
|
||||||
return $this->encode();
|
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));
|
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 {
|
public static function fromFile(string $path): BencodedContent {
|
||||||
return self::fromEncoded(FileStream::openRead($path));
|
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 {
|
public static function fromRequest(): BencodedContent {
|
||||||
return self::fromFile('php://input');
|
return self::fromFile('php://input');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,50 +1,102 @@
|
||||||
<?php
|
<?php
|
||||||
// FormContent.php
|
// FormContent.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2023-08-22
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Index\Http\HttpUploadedFile;
|
use Index\Http\HttpUploadedFile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents form body content for a HTTP message.
|
||||||
|
*/
|
||||||
class FormContent implements IHttpContent {
|
class FormContent implements IHttpContent {
|
||||||
private array $postFields;
|
private array $postFields;
|
||||||
private array $uploadedFiles;
|
private array $uploadedFiles;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $postFields Form fields.
|
||||||
|
* @param array<string, HttpUploadedFile> $uploadedFiles Uploaded files.
|
||||||
|
*/
|
||||||
public function __construct(array $postFields, array $uploadedFiles) {
|
public function __construct(array $postFields, array $uploadedFiles) {
|
||||||
$this->postFields = $postFields;
|
$this->postFields = $postFields;
|
||||||
$this->uploadedFiles = $uploadedFiles;
|
$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 {
|
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||||
if(!isset($this->postFields[$name]))
|
if(!isset($this->postFields[$name]))
|
||||||
return null;
|
return null;
|
||||||
return filter_var($this->postFields[$name] ?? null, $filter, $options);
|
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 {
|
public function hasParam(string $name): bool {
|
||||||
return isset($this->postFields[$name]);
|
return isset($this->postFields[$name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all form fields.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> Form fields.
|
||||||
|
*/
|
||||||
public function getParams(): array {
|
public function getParams(): array {
|
||||||
return $this->postFields;
|
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 {
|
public function getParamString(bool $spacesAsPlus = false): string {
|
||||||
return http_build_query($this->postFields, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
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 {
|
public function hasUploadedFile(string $name): bool {
|
||||||
return isset($this->uploadedFiles[$name]);
|
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 {
|
public function getUploadedFile(string $name): HttpUploadedFile {
|
||||||
if(!isset($this->uploadedFiles[$name]))
|
if(!isset($this->uploadedFiles[$name]))
|
||||||
throw new RuntimeException('No file with name $name present.');
|
throw new RuntimeException('No file with name $name present.');
|
||||||
return $this->uploadedFiles[$name];
|
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 {
|
public static function fromRaw(array $post, array $files): FormContent {
|
||||||
return new FormContent(
|
return new FormContent(
|
||||||
$post,
|
$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 {
|
public static function fromRequest(): FormContent {
|
||||||
return self::fromRaw($_POST, $_FILES);
|
return self::fromRaw($_POST, $_FILES);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
// IHttpContent.php
|
// IHttpContent.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
interface IHttpContent extends Stringable {
|
/**
|
||||||
}
|
* Represents the body content for a HTTP message.
|
||||||
|
*/
|
||||||
|
interface IHttpContent extends Stringable {}
|
||||||
|
|
|
@ -1,22 +1,32 @@
|
||||||
<?php
|
<?php
|
||||||
// JsonContent.php
|
// JsonContent.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2023-07-21
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use JsonSerializable;
|
use JsonSerializable;
|
||||||
use Stringable;
|
|
||||||
use Index\IO\Stream;
|
use Index\IO\Stream;
|
||||||
use Index\IO\FileStream;
|
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;
|
private mixed $content;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $content Content to be JSON encoded.
|
||||||
|
*/
|
||||||
public function __construct(mixed $content) {
|
public function __construct(mixed $content) {
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves unencoded content.
|
||||||
|
*
|
||||||
|
* @return mixed Content.
|
||||||
|
*/
|
||||||
public function getContent(): mixed {
|
public function getContent(): mixed {
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +35,11 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the content.
|
||||||
|
*
|
||||||
|
* @return string JSON encoded string.
|
||||||
|
*/
|
||||||
public function encode(): string {
|
public function encode(): string {
|
||||||
return json_encode($this->content);
|
return json_encode($this->content);
|
||||||
}
|
}
|
||||||
|
@ -33,14 +48,31 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
|
||||||
return $this->encode();
|
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));
|
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 {
|
public static function fromFile(string $path): JsonContent {
|
||||||
return self::fromEncoded(FileStream::openRead($path));
|
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 {
|
public static function fromRequest(): JsonContent {
|
||||||
return self::fromFile('php://input');
|
return self::fromFile('php://input');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,31 @@
|
||||||
<?php
|
<?php
|
||||||
// StreamContent.php
|
// StreamContent.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
use Index\IO\Stream;
|
use Index\IO\Stream;
|
||||||
use Index\IO\FileStream;
|
use Index\IO\FileStream;
|
||||||
|
|
||||||
class StreamContent implements Stringable, IHttpContent {
|
/**
|
||||||
|
* Represents Stream body content for a HTTP message.
|
||||||
|
*/
|
||||||
|
class StreamContent implements IHttpContent {
|
||||||
private Stream $stream;
|
private Stream $stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Stream $stream Stream that represents this message body.
|
||||||
|
*/
|
||||||
public function __construct(Stream $stream) {
|
public function __construct(Stream $stream) {
|
||||||
$this->stream = $stream;
|
$this->stream = $stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the underlying stream.
|
||||||
|
*
|
||||||
|
* @return Stream Underlying stream.
|
||||||
|
*/
|
||||||
public function getStream(): Stream {
|
public function getStream(): Stream {
|
||||||
return $this->stream;
|
return $this->stream;
|
||||||
}
|
}
|
||||||
|
@ -24,12 +34,23 @@ class StreamContent implements Stringable, IHttpContent {
|
||||||
return (string)$this->stream;
|
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 {
|
public static function fromFile(string $path): StreamContent {
|
||||||
return new StreamContent(
|
return new StreamContent(
|
||||||
FileStream::openRead($path)
|
FileStream::openRead($path)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an instance from the raw request body.
|
||||||
|
*
|
||||||
|
* @return StreamContent Instance representing the request body.
|
||||||
|
*/
|
||||||
public static function fromRequest(): StreamContent {
|
public static function fromRequest(): StreamContent {
|
||||||
return self::fromFile('php://input');
|
return self::fromFile('php://input');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,30 @@
|
||||||
<?php
|
<?php
|
||||||
// StringContent.php
|
// StringContent.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Content;
|
namespace Index\Http\Content;
|
||||||
|
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
class StringContent implements Stringable, IHttpContent {
|
/**
|
||||||
|
* Represents string body content for a HTTP message.
|
||||||
|
*/
|
||||||
|
class StringContent implements IHttpContent {
|
||||||
private string $string;
|
private string $string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $string String that represents this message body.
|
||||||
|
*/
|
||||||
public function __construct(string $string) {
|
public function __construct(string $string) {
|
||||||
$this->string = $string;
|
$this->string = $string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the underlying string.
|
||||||
|
*
|
||||||
|
* @return string Underlying string.
|
||||||
|
*/
|
||||||
public function getString(): string {
|
public function getString(): string {
|
||||||
return $this->string;
|
return $this->string;
|
||||||
}
|
}
|
||||||
|
@ -22,14 +33,31 @@ class StringContent implements Stringable, IHttpContent {
|
||||||
return $this->string;
|
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 {
|
public static function fromFile(string $path): StringContent {
|
||||||
return new StringContent(file_get_contents($path));
|
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 {
|
public static function fromRequest(): StringContent {
|
||||||
return self::fromFile('php://input');
|
return self::fromFile('php://input');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// BencodeContentHandler.php
|
// BencodeContentHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ContentHandling;
|
namespace Index\Http\ContentHandling;
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ use Index\Bencode\IBencodeSerialisable;
|
||||||
use Index\Http\HttpResponseBuilder;
|
use Index\Http\HttpResponseBuilder;
|
||||||
use Index\Http\Content\BencodedContent;
|
use Index\Http\Content\BencodedContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Bencode content handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
class BencodeContentHandler implements IContentHandler {
|
class BencodeContentHandler implements IContentHandler {
|
||||||
public function match(mixed $content): bool {
|
public function match(mixed $content): bool {
|
||||||
return $content instanceof IBencodeSerialisable;
|
return $content instanceof IBencodeSerialisable;
|
||||||
|
|
|
@ -1,13 +1,29 @@
|
||||||
<?php
|
<?php
|
||||||
// IContentHandler.php
|
// IContentHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ContentHandling;
|
namespace Index\Http\ContentHandling;
|
||||||
|
|
||||||
use Index\Http\HttpResponseBuilder;
|
use Index\Http\HttpResponseBuilder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a content handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
interface IContentHandler {
|
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;
|
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;
|
function handle(HttpResponseBuilder $response, mixed $content): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// JsonContentHandler.php
|
// JsonContentHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ContentHandling;
|
namespace Index\Http\ContentHandling;
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ use JsonSerializable;
|
||||||
use Index\Http\HttpResponseBuilder;
|
use Index\Http\HttpResponseBuilder;
|
||||||
use Index\Http\Content\JsonContent;
|
use Index\Http\Content\JsonContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a JSON content handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
class JsonContentHandler implements IContentHandler {
|
class JsonContentHandler implements IContentHandler {
|
||||||
public function match(mixed $content): bool {
|
public function match(mixed $content): bool {
|
||||||
return is_array($content) || $content instanceof JsonSerializable || $content instanceof stdClass;
|
return is_array($content) || $content instanceof JsonSerializable || $content instanceof stdClass;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// StreamContentHandler.php
|
// StreamContentHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ContentHandling;
|
namespace Index\Http\ContentHandling;
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ use Index\Http\HttpResponseBuilder;
|
||||||
use Index\Http\Content\StreamContent;
|
use Index\Http\Content\StreamContent;
|
||||||
use Index\IO\Stream;
|
use Index\IO\Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Stream content handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
class StreamContentHandler implements IContentHandler {
|
class StreamContentHandler implements IContentHandler {
|
||||||
public function match(mixed $content): bool {
|
public function match(mixed $content): bool {
|
||||||
return $content instanceof Stream;
|
return $content instanceof Stream;
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
// HtmlErrorHandler.php
|
// HtmlErrorHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ErrorHandling;
|
namespace Index\Http\ErrorHandling;
|
||||||
|
|
||||||
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a basic HTML error message handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
class HtmlErrorHandler implements IErrorHandler {
|
class HtmlErrorHandler implements IErrorHandler {
|
||||||
private const TEMPLATE = <<<HTML
|
private const TEMPLATE = <<<HTML
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
|
@ -1,13 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
// IErrorHandler.php
|
// IErrorHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ErrorHandling;
|
namespace Index\Http\ErrorHandling;
|
||||||
|
|
||||||
use Index\Http\HttpResponseBuilder;
|
use Index\Http\HttpResponseBuilder;
|
||||||
use Index\Http\HttpRequest;
|
use Index\Http\HttpRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an error message handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
interface IErrorHandler {
|
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;
|
function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
// PlainErrorHandler.php
|
// PlainErrorHandler.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\ErrorHandling;
|
namespace Index\Http\ErrorHandling;
|
||||||
|
|
||||||
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
use Index\Http\{HttpResponseBuilder,HttpRequest};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a plain text error message handler for building HTTP response messages.
|
||||||
|
*/
|
||||||
class PlainErrorHandler implements IErrorHandler {
|
class PlainErrorHandler implements IErrorHandler {
|
||||||
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
|
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
|
||||||
$response->setTypePlain();
|
$response->setTypePlain();
|
||||||
|
|
|
@ -1,29 +1,51 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpHeader.php
|
// HttpHeader.php
|
||||||
// Created: 2022-02-14
|
// Created: 2022-02-14
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic HTTP header.
|
||||||
|
*/
|
||||||
class HttpHeader implements Stringable {
|
class HttpHeader implements Stringable {
|
||||||
private string $name;
|
private string $name;
|
||||||
private array $lines;
|
private array $lines;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name Name of the header.
|
||||||
|
* @param string ...$lines Lines of the header.
|
||||||
|
*/
|
||||||
public function __construct(string $name, string ...$lines) {
|
public function __construct(string $name, string ...$lines) {
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->lines = $lines;
|
$this->lines = $lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the name of the header.
|
||||||
|
*
|
||||||
|
* @return string Header name.
|
||||||
|
*/
|
||||||
public function getName(): string {
|
public function getName(): string {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves lines of the header.
|
||||||
|
*
|
||||||
|
* @return string[] Header lines.
|
||||||
|
*/
|
||||||
public function getLines(): array {
|
public function getLines(): array {
|
||||||
return $this->lines;
|
return $this->lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the first line of the header.
|
||||||
|
*
|
||||||
|
* @return string First header line.
|
||||||
|
*/
|
||||||
public function getFirstLine(): string {
|
public function getFirstLine(): string {
|
||||||
return $this->lines[0];
|
return $this->lines[0];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpHeaders.php
|
// HttpHeaders.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a collection of HTTP headers.
|
||||||
|
*/
|
||||||
class HttpHeaders {
|
class HttpHeaders {
|
||||||
private array $headers;
|
private array $headers;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param HttpHeader[] $headers HTTP header instances.
|
||||||
|
*/
|
||||||
public function __construct(array $headers) {
|
public function __construct(array $headers) {
|
||||||
$real = [];
|
$real = [];
|
||||||
|
|
||||||
|
@ -20,14 +26,32 @@ class HttpHeaders {
|
||||||
$this->headers = $real;
|
$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 {
|
public function hasHeader(string $name): bool {
|
||||||
return isset($this->headers[strtolower($name)]);
|
return isset($this->headers[strtolower($name)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all headers.
|
||||||
|
*
|
||||||
|
* @return HttpHeader[] All headers.
|
||||||
|
*/
|
||||||
public function getHeaders(): array {
|
public function getHeaders(): array {
|
||||||
return array_values($this->headers);
|
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 {
|
public function getHeader(string $name): HttpHeader {
|
||||||
$name = strtolower($name);
|
$name = strtolower($name);
|
||||||
if(!isset($this->headers[$name]))
|
if(!isset($this->headers[$name]))
|
||||||
|
@ -36,18 +60,36 @@ class HttpHeaders {
|
||||||
return $this->headers[$name];
|
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 {
|
public function getHeaderLine(string $name): string {
|
||||||
if(!$this->hasHeader($name))
|
if(!$this->hasHeader($name))
|
||||||
return '';
|
return '';
|
||||||
return (string)$this->getHeader($name);
|
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 {
|
public function getHeaderLines(string $name): array {
|
||||||
if(!$this->hasHeader($name))
|
if(!$this->hasHeader($name))
|
||||||
return [];
|
return [];
|
||||||
return $this->getHeader($name)->getLines();
|
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 {
|
public function getHeaderFirstLine(string $name): string {
|
||||||
if(!$this->hasHeader($name))
|
if(!$this->hasHeader($name))
|
||||||
return '';
|
return '';
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpHeadersBuilder.php
|
// HttpHeadersBuilder.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP message header builder.
|
||||||
|
*/
|
||||||
class HttpHeadersBuilder {
|
class HttpHeadersBuilder {
|
||||||
private array $headers = [];
|
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 {
|
public function addHeader(string $name, mixed $value): void {
|
||||||
$nameLower = strtolower($name);
|
$nameLower = strtolower($name);
|
||||||
if(!isset($this->headers[$nameLower]))
|
if(!isset($this->headers[$nameLower]))
|
||||||
|
@ -17,18 +27,41 @@ class HttpHeadersBuilder {
|
||||||
$this->headers[$nameLower][] = $value;
|
$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 {
|
public function setHeader(string $name, mixed $value): void {
|
||||||
$this->headers[strtolower($name)] = [$name, $value];
|
$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 {
|
public function removeHeader(string $name): void {
|
||||||
unset($this->headers[strtolower($name)]);
|
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 {
|
public function hasHeader(string $name): bool {
|
||||||
return isset($this->headers[strtolower($name)]);
|
return isset($this->headers[strtolower($name)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HttpHeaders instance from this builder.
|
||||||
|
*
|
||||||
|
* @return HttpHeaders Instance containing HTTP headers.
|
||||||
|
*/
|
||||||
public function toHeaders(): HttpHeaders {
|
public function toHeaders(): HttpHeaders {
|
||||||
$headers = [];
|
$headers = [];
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpMessage.php
|
// HttpMessage.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
use Index\IO\Stream;
|
use Index\IO\Stream;
|
||||||
use Index\Http\Content\IHttpContent;
|
use Index\Http\Content\IHttpContent;
|
||||||
use Index\Http\Content\BencodedContent;
|
use Index\Http\Content\BencodedContent;
|
||||||
|
@ -13,69 +14,153 @@ use Index\Http\Content\JsonContent;
|
||||||
use Index\Http\Content\StreamContent;
|
use Index\Http\Content\StreamContent;
|
||||||
use Index\Http\Content\StringContent;
|
use Index\Http\Content\StringContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a base HTTP message.
|
||||||
|
*/
|
||||||
abstract class HttpMessage {
|
abstract class HttpMessage {
|
||||||
private string $version;
|
private string $version;
|
||||||
private HttpHeaders $headers;
|
private HttpHeaders $headers;
|
||||||
private ?IHttpContent $content;
|
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) {
|
public function __construct(string $version, HttpHeaders $headers, ?IHttpContent $content) {
|
||||||
$this->version = $version;
|
$this->version = $version;
|
||||||
$this->headers = $headers;
|
$this->headers = $headers;
|
||||||
$this->content = $content;
|
$this->content = $content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP version of this message.
|
||||||
|
*
|
||||||
|
* @return string HTTP version.
|
||||||
|
*/
|
||||||
public function getHttpVersion(): string {
|
public function getHttpVersion(): string {
|
||||||
return $this->version;
|
return $this->version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the collection of headers for this HTTP message.
|
||||||
|
*
|
||||||
|
* @return HttpHeader[] HTTP headers.
|
||||||
|
*/
|
||||||
public function getHeaders(): array {
|
public function getHeaders(): array {
|
||||||
return $this->headers->getHeaders();
|
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 {
|
public function hasHeader(string $name): bool {
|
||||||
return $this->headers->hasHeader($name);
|
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 {
|
public function getHeader(string $name): HttpHeader {
|
||||||
return $this->headers->getHeader($name);
|
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 {
|
public function getHeaderLine(string $name): string {
|
||||||
return $this->headers->getHeaderLine($name);
|
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 {
|
public function getHeaderLines(string $name): array {
|
||||||
return $this->headers->getHeaderLines($name);
|
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 {
|
public function getHeaderFirstLine(string $name): string {
|
||||||
return $this->headers->getHeaderFirstLine($name);
|
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 {
|
public function hasContent(): bool {
|
||||||
return $this->content !== null;
|
return $this->content !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves message body contents, if present.
|
||||||
|
*
|
||||||
|
* @return ?IHttpContent Body contents, null if none present.
|
||||||
|
*/
|
||||||
public function getContent(): ?IHttpContent {
|
public function getContent(): ?IHttpContent {
|
||||||
return $this->content;
|
return $this->content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the body content is JsonContent.
|
||||||
|
*
|
||||||
|
* @return bool true if it is JsonContent.
|
||||||
|
*/
|
||||||
public function isJsonContent(): bool {
|
public function isJsonContent(): bool {
|
||||||
return $this->content instanceof JsonContent;
|
return $this->content instanceof JsonContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the body content is FormContent.
|
||||||
|
*
|
||||||
|
* @return bool true if it is FormContent.
|
||||||
|
*/
|
||||||
public function isFormContent(): bool {
|
public function isFormContent(): bool {
|
||||||
return $this->content instanceof FormContent;
|
return $this->content instanceof FormContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the body content is StreamContent.
|
||||||
|
*
|
||||||
|
* @return bool true if it is StreamContent.
|
||||||
|
*/
|
||||||
public function isStreamContent(): bool {
|
public function isStreamContent(): bool {
|
||||||
return $this->content instanceof StreamContent;
|
return $this->content instanceof StreamContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the body content is StringContent.
|
||||||
|
*
|
||||||
|
* @return bool true if it is StringContent.
|
||||||
|
*/
|
||||||
public function isStringContent(): bool {
|
public function isStringContent(): bool {
|
||||||
return $this->content instanceof StringContent;
|
return $this->content instanceof StringContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the body content is BencodedContent.
|
||||||
|
*
|
||||||
|
* @return bool true if it is BencodedContent.
|
||||||
|
*/
|
||||||
public function isBencodedContent(): bool {
|
public function isBencodedContent(): bool {
|
||||||
return $this->content instanceof BencodedContent;
|
return $this->content instanceof BencodedContent;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpMessageBuilder.php
|
// HttpMessageBuilder.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ use Index\Http\Content\IHttpContent;
|
||||||
use Index\Http\Content\StreamContent;
|
use Index\Http\Content\StreamContent;
|
||||||
use Index\Http\Content\StringContent;
|
use Index\Http\Content\StringContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a base HTTP message builder.
|
||||||
|
*/
|
||||||
class HttpMessageBuilder {
|
class HttpMessageBuilder {
|
||||||
private ?string $version = null;
|
private ?string $version = null;
|
||||||
private HttpHeadersBuilder $headers;
|
private HttpHeadersBuilder $headers;
|
||||||
|
@ -19,47 +22,107 @@ class HttpMessageBuilder {
|
||||||
$this->headers = new HttpHeadersBuilder;
|
$this->headers = new HttpHeadersBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP version of the message.
|
||||||
|
*
|
||||||
|
* @return string HTTP version.
|
||||||
|
*/
|
||||||
protected function getHttpVersion(): string {
|
protected function getHttpVersion(): string {
|
||||||
return $this->version ?? '1.1';
|
return $this->version ?? '1.1';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets HTTP version for the message.
|
||||||
|
*
|
||||||
|
* @param string $version HTTP version.
|
||||||
|
*/
|
||||||
public function setHttpVersion(string $version): void {
|
public function setHttpVersion(string $version): void {
|
||||||
$this->version = $version;
|
$this->version = $version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create HttpHeaders instance from this builder.
|
||||||
|
*
|
||||||
|
* @return HttpHeaders Instance containing HTTP headers.
|
||||||
|
*/
|
||||||
protected function getHeaders(): HttpHeaders {
|
protected function getHeaders(): HttpHeaders {
|
||||||
return $this->headers->toHeaders();
|
return $this->headers->toHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves instance of the underlying HTTP header builder.
|
||||||
|
*
|
||||||
|
* @return HttpHeadersBuilder HTTP header builder.
|
||||||
|
*/
|
||||||
public function getHeadersBuilder(): HttpHeadersBuilder {
|
public function getHeadersBuilder(): HttpHeadersBuilder {
|
||||||
return $this->headers;
|
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 {
|
public function addHeader(string $name, mixed $value): void {
|
||||||
$this->headers->addHeader($name, $value);
|
$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 {
|
public function setHeader(string $name, mixed $value): void {
|
||||||
$this->headers->setHeader($name, $value);
|
$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 {
|
public function removeHeader(string $name): void {
|
||||||
$this->headers->removeHeader($name);
|
$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 {
|
public function hasHeader(string $name): bool {
|
||||||
return $this->headers->hasHeader($name);
|
return $this->headers->hasHeader($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves HTTP message body content.
|
||||||
|
*
|
||||||
|
* @return ?IHttpContent Body content.
|
||||||
|
*/
|
||||||
protected function getContent(): ?IHttpContent {
|
protected function getContent(): ?IHttpContent {
|
||||||
return $this->content;
|
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 {
|
public function hasContent(): bool {
|
||||||
return $this->content !== null;
|
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 {
|
public function setContent(IHttpContent|Stream|string|null $content): void {
|
||||||
if($content instanceof Stream)
|
if($content instanceof Stream)
|
||||||
$content = new StreamContent($content);
|
$content = new StreamContent($content);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpRequest.php
|
// HttpRequest.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
@ -13,12 +13,24 @@ use Index\Http\Content\JsonContent;
|
||||||
use Index\Http\Content\StreamContent;
|
use Index\Http\Content\StreamContent;
|
||||||
use Index\Http\Content\FormContent;
|
use Index\Http\Content\FormContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP request message.
|
||||||
|
*/
|
||||||
class HttpRequest extends HttpMessage {
|
class HttpRequest extends HttpMessage {
|
||||||
private string $method;
|
private string $method;
|
||||||
private string $path;
|
private string $path;
|
||||||
private array $params;
|
private array $params;
|
||||||
private array $cookies;
|
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(
|
public function __construct(
|
||||||
string $version,
|
string $version,
|
||||||
string $method,
|
string $method,
|
||||||
|
@ -36,46 +48,105 @@ class HttpRequest extends HttpMessage {
|
||||||
parent::__construct($version, $headers, $content);
|
parent::__construct($version, $headers, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP request method.
|
||||||
|
*
|
||||||
|
* @return string HTTP request method.
|
||||||
|
*/
|
||||||
public function getMethod(): string {
|
public function getMethod(): string {
|
||||||
return $this->method;
|
return $this->method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP request path.
|
||||||
|
*
|
||||||
|
* @return string HTTP request path.
|
||||||
|
*/
|
||||||
public function getPath(): string {
|
public function getPath(): string {
|
||||||
return $this->path;
|
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 {
|
public function getParamString(bool $spacesAsPlus = false): string {
|
||||||
return http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
|
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 {
|
public function getParams(): array {
|
||||||
return $this->params;
|
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 {
|
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||||
if(!isset($this->params[$name]))
|
if(!isset($this->params[$name]))
|
||||||
return null;
|
return null;
|
||||||
return filter_var($this->params[$name] ?? null, $filter, $options);
|
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 {
|
public function hasParam(string $name): bool {
|
||||||
return isset($this->params[$name]);
|
return isset($this->params[$name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves all HTTP request cookies.
|
||||||
|
*
|
||||||
|
* @return array<string, string> All cookies.
|
||||||
|
*/
|
||||||
public function getCookies(): array {
|
public function getCookies(): array {
|
||||||
return $this->cookies;
|
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 {
|
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
|
||||||
if(!isset($this->cookies[$name]))
|
if(!isset($this->cookies[$name]))
|
||||||
return null;
|
return null;
|
||||||
return filter_var($this->cookies[$name] ?? null, $filter, $options);
|
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 {
|
public function hasCookie(string $name): bool {
|
||||||
return isset($this->cookies[$name]);
|
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 {
|
public static function fromRequest(): HttpRequest {
|
||||||
$build = new HttpRequestBuilder;
|
$build = new HttpRequestBuilder;
|
||||||
$build->setHttpVersion($_SERVER['SERVER_PROTOCOL']);
|
$build->setHttpVersion($_SERVER['SERVER_PROTOCOL']);
|
||||||
|
|
|
@ -1,64 +1,134 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpRequestBuilder.php
|
// HttpRequestBuilder.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP request message builder.
|
||||||
|
*/
|
||||||
class HttpRequestBuilder extends HttpMessageBuilder {
|
class HttpRequestBuilder extends HttpMessageBuilder {
|
||||||
private string $method = 'GET';
|
private string $method = 'GET';
|
||||||
private string $path = '/';
|
private string $path = '/';
|
||||||
private array $params = [];
|
private array $params = [];
|
||||||
private array $cookies = [];
|
private array $cookies = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP request method.
|
||||||
|
*
|
||||||
|
* @return string HTTP request method.
|
||||||
|
*/
|
||||||
protected function getMethod(): string {
|
protected function getMethod(): string {
|
||||||
return $this->method;
|
return $this->method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets HTTP request method.
|
||||||
|
*
|
||||||
|
* @param string $method HTTP request method.
|
||||||
|
*/
|
||||||
public function setMethod(string $method): void {
|
public function setMethod(string $method): void {
|
||||||
$this->method = $method;
|
$this->method = $method;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP request path.
|
||||||
|
*
|
||||||
|
* @return string HTTP request path.
|
||||||
|
*/
|
||||||
protected function getPath(): string {
|
protected function getPath(): string {
|
||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets HTTP request path.
|
||||||
|
*
|
||||||
|
* @param string $path HTTP request path.
|
||||||
|
*/
|
||||||
public function setPath(string $path): void {
|
public function setPath(string $path): void {
|
||||||
$this->path = $path;
|
$this->path = $path;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP request query params.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed> HTTP request query params.
|
||||||
|
*/
|
||||||
protected function getParams(): array {
|
protected function getParams(): array {
|
||||||
return $this->params;
|
return $this->params;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets HTTP request query params.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $params HTTP request query params.
|
||||||
|
*/
|
||||||
public function setParams(array $params): void {
|
public function setParams(array $params): void {
|
||||||
$this->params = $params;
|
$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 {
|
public function setParam(string $name, mixed $value): void {
|
||||||
$this->params[$name] = $value;
|
$this->params[$name] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a HTTP request query param.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the query field.
|
||||||
|
*/
|
||||||
public function removeParam(string $name): void {
|
public function removeParam(string $name): void {
|
||||||
unset($this->params[$name]);
|
unset($this->params[$name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP request cookies.
|
||||||
|
*
|
||||||
|
* @return array<string, string> HTTP request cookies.
|
||||||
|
*/
|
||||||
protected function getCookies(): array {
|
protected function getCookies(): array {
|
||||||
return $this->cookies;
|
return $this->cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets HTTP request cookies.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $cookies HTTP request cookies.
|
||||||
|
*/
|
||||||
public function setCookies(array $cookies): void {
|
public function setCookies(array $cookies): void {
|
||||||
$this->cookies = $cookies;
|
$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 {
|
public function setCookie(string $name, mixed $value): void {
|
||||||
$this->cookies[$name] = $value;
|
$this->cookies[$name] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a HTTP request cookie.
|
||||||
|
*
|
||||||
|
* @param string $name Name of the cookie.
|
||||||
|
*/
|
||||||
public function removeCookie(string $name): void {
|
public function removeCookie(string $name): void {
|
||||||
unset($this->cookies[$name]);
|
unset($this->cookies[$name]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HttpRequest instance from this builder.
|
||||||
|
*
|
||||||
|
* @return HttpRequest An instance representing this builder.
|
||||||
|
*/
|
||||||
public function toRequest(): HttpRequest {
|
public function toRequest(): HttpRequest {
|
||||||
return new HttpRequest(
|
return new HttpRequest(
|
||||||
$this->getHttpVersion(),
|
$this->getHttpVersion(),
|
||||||
|
|
|
@ -1,16 +1,26 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpResponse.php
|
// HttpResponse.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
use Index\Http\Content\IHttpContent;
|
use Index\Http\Content\IHttpContent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP response message.
|
||||||
|
*/
|
||||||
class HttpResponse extends HttpMessage {
|
class HttpResponse extends HttpMessage {
|
||||||
private int $statusCode;
|
private int $statusCode;
|
||||||
private string $statusText;
|
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(
|
public function __construct(
|
||||||
string $version,
|
string $version,
|
||||||
int $statusCode,
|
int $statusCode,
|
||||||
|
@ -24,10 +34,20 @@ class HttpResponse extends HttpMessage {
|
||||||
parent::__construct($version, $headers, $content);
|
parent::__construct($version, $headers, $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP response message status code.
|
||||||
|
*
|
||||||
|
* @return int HTTP status code.
|
||||||
|
*/
|
||||||
public function getStatusCode(): int {
|
public function getStatusCode(): int {
|
||||||
return $this->statusCode;
|
return $this->statusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP response message status text.
|
||||||
|
*
|
||||||
|
* @return string HTTP status text.
|
||||||
|
*/
|
||||||
public function getStatusText(): string {
|
public function getStatusText(): string {
|
||||||
return $this->statusText;
|
return $this->statusText;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpResponseBuilder.php
|
// HttpResponseBuilder.php
|
||||||
// Created: 2022-02-08
|
// Created: 2022-02-08
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
@ -11,35 +11,78 @@ use Index\MediaType;
|
||||||
use Index\XDateTime;
|
use Index\XDateTime;
|
||||||
use Index\Performance\Timings;
|
use Index\Performance\Timings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a HTTP response message builder.
|
||||||
|
*/
|
||||||
class HttpResponseBuilder extends HttpMessageBuilder {
|
class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
private int $statusCode = -1;
|
private int $statusCode = -1;
|
||||||
private ?string $statusText;
|
private ?string $statusText;
|
||||||
private array $vary = [];
|
private array $vary = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the HTTP status code for the target HTTP response message.
|
||||||
|
*
|
||||||
|
* @return int HTTP status code.
|
||||||
|
*/
|
||||||
public function getStatusCode(): int {
|
public function getStatusCode(): int {
|
||||||
return $this->statusCode < 0 ? 200 : $this->statusCode;
|
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 {
|
public function setStatusCode(int $statusCode): void {
|
||||||
$this->statusCode = $statusCode;
|
$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 {
|
public function hasStatusCode(): bool {
|
||||||
return $this->statusCode >= 100;
|
return $this->statusCode >= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the status text for this response.
|
||||||
|
*
|
||||||
|
* @return string Status text.
|
||||||
|
*/
|
||||||
public function getStatusText(): string {
|
public function getStatusText(): string {
|
||||||
return $this->statusText ?? self::STATUS[$this->getStatusCode()] ?? 'Unknown Status';
|
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 {
|
public function setStatusText(string $statusText): void {
|
||||||
$this->statusText = (string)$statusText;
|
$this->statusText = (string)$statusText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the status text for this message.
|
||||||
|
*/
|
||||||
public function clearStatusText(): void {
|
public function clearStatusText(): void {
|
||||||
$this->statusText = null;
|
$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(
|
public function addCookie(
|
||||||
string $name,
|
string $name,
|
||||||
mixed $value,
|
mixed $value,
|
||||||
|
@ -71,6 +114,16 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
$this->addHeader('Set-Cookie', $cookie);
|
$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(
|
public function removeCookie(
|
||||||
string $name,
|
string $name,
|
||||||
string $path = '',
|
string $path = '',
|
||||||
|
@ -82,11 +135,22 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
$this->addCookie($name, '', -9001, $path, $domain, $secure, $httpOnly, $sameSiteStrict);
|
$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 {
|
public function redirect(string $to, bool $permanent = false): void {
|
||||||
$this->setStatusCode($permanent ? 301 : 302);
|
$this->setStatusCode($permanent ? 301 : 302);
|
||||||
$this->setHeader('Location', $to);
|
$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 {
|
public function addVary(string|array $headers): void {
|
||||||
if(!is_array($headers))
|
if(!is_array($headers))
|
||||||
$headers = [$headers];
|
$headers = [$headers];
|
||||||
|
@ -100,10 +164,21 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
$this->setHeader('Vary', implode(', ', $this->vary));
|
$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 {
|
public function setPoweredBy(string $poweredBy): void {
|
||||||
$this->setHeader('X-Powered-By', $poweredBy);
|
$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 {
|
public function setEntityTag(string $eTag, bool $weak = false): void {
|
||||||
$eTag = '"' . $eTag . '"';
|
$eTag = '"' . $eTag . '"';
|
||||||
|
|
||||||
|
@ -113,6 +188,11 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
$this->setHeader('ETag', $eTag);
|
$this->setHeader('ETag', $eTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a Server-Timing header.
|
||||||
|
*
|
||||||
|
* @param Timings $timings Timings to supply to the devtools.
|
||||||
|
*/
|
||||||
public function setServerTiming(Timings $timings): void {
|
public function setServerTiming(Timings $timings): void {
|
||||||
$laps = $timings->getLaps();
|
$laps = $timings->getLaps();
|
||||||
$timings = [];
|
$timings = [];
|
||||||
|
@ -129,50 +209,109 @@ class HttpResponseBuilder extends HttpMessageBuilder {
|
||||||
$this->setHeader('Server-Timing', implode(', ', $timings));
|
$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 {
|
public function hasContentType(): bool {
|
||||||
return $this->hasHeader('Content-Type');
|
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 {
|
public function setContentType(MediaType|string $mediaType): void {
|
||||||
$this->setHeader('Content-Type', (string)$mediaType);
|
$this->setHeader('Content-Type', (string)$mediaType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Content-Type to 'application/octet-stream' for raw content.
|
||||||
|
*/
|
||||||
public function setTypeStream(): void {
|
public function setTypeStream(): void {
|
||||||
$this->setContentType('application/octet-stream');
|
$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 {
|
public function setTypePlain(string $charset = 'us-ascii'): void {
|
||||||
$this->setContentType('text/plain; charset=' . $charset);
|
$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 {
|
public function setTypeHTML(string $charset = 'utf-8'): void {
|
||||||
$this->setContentType('text/html; charset=' . $charset);
|
$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 {
|
public function setTypeJson(string $charset = 'utf-8'): void {
|
||||||
$this->setContentType('application/json; charset=' . $charset);
|
$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 {
|
public function setTypeXML(string $charset = 'utf-8'): void {
|
||||||
$this->setContentType('application/xml; charset=' . $charset);
|
$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 {
|
public function setTypeCSS(string $charset = 'utf-8'): void {
|
||||||
$this->setContentType('text/css; charset=' . $charset);
|
$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 {
|
public function setTypeJS(string $charset = 'utf-8'): void {
|
||||||
$this->setContentType('application/javascript; charset=' . $charset);
|
$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 {
|
public function sendFile(string $absolutePath): void {
|
||||||
$this->setHeader('X-Sendfile', $absolutePath);
|
$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 {
|
public function accelRedirect(string $uri): void {
|
||||||
$this->setHeader('X-Accel-Redirect', $uri);
|
$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 {
|
public function setFileName(string $fileName, bool $attachment = false): void {
|
||||||
$this->setHeader(
|
$this->setHeader(
|
||||||
'Content-Disposition',
|
'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 {
|
public function clearSiteData(string ...$directives): void {
|
||||||
$this->setHeader(
|
$this->setHeader(
|
||||||
'Clear-Site-Data',
|
'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 {
|
public function setCacheControl(string ...$directives): void {
|
||||||
$this->setHeader('Cache-Control', implode(', ', $directives));
|
$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 {
|
public function toResponse(): HttpResponse {
|
||||||
return new HttpResponse(
|
return new HttpResponse(
|
||||||
$this->getHttpVersion(),
|
$this->getHttpVersion(),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpUploadedFile.php
|
// HttpUploadedFile.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http;
|
namespace Index\Http;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Index\IO\FileStream;
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an uploaded file in a multipart/form-data request.
|
||||||
|
*/
|
||||||
class HttpUploadedFile implements ICloseable {
|
class HttpUploadedFile implements ICloseable {
|
||||||
private int $errorCode;
|
private int $errorCode;
|
||||||
private int $size;
|
private int $size;
|
||||||
|
@ -21,6 +24,13 @@ class HttpUploadedFile implements ICloseable {
|
||||||
private bool $hasMoved = false;
|
private bool $hasMoved = false;
|
||||||
private ?Stream $stream = null;
|
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(
|
public function __construct(
|
||||||
int $errorCode,
|
int $errorCode,
|
||||||
int $size,
|
int $size,
|
||||||
|
@ -38,34 +48,75 @@ class HttpUploadedFile implements ICloseable {
|
||||||
$this->suggestedMediaType = $suggestedMediaType;
|
$this->suggestedMediaType = $suggestedMediaType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the PHP file upload error code.
|
||||||
|
*
|
||||||
|
* @return int PHP file upload error code.
|
||||||
|
*/
|
||||||
public function getErrorCode(): int {
|
public function getErrorCode(): int {
|
||||||
return $this->errorCode;
|
return $this->errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the size of the uploaded file.
|
||||||
|
*
|
||||||
|
* @return int Size of uploaded file.
|
||||||
|
*/
|
||||||
public function getSize(): int {
|
public function getSize(): int {
|
||||||
return $this->size;
|
return $this->size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the local path to the uploaded file.
|
||||||
|
*
|
||||||
|
* @return ?string Path to file, or null.
|
||||||
|
*/
|
||||||
public function getLocalFileName(): ?string {
|
public function getLocalFileName(): ?string {
|
||||||
return $this->localFileName;
|
return $this->localFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves media type of the uploaded file.
|
||||||
|
*
|
||||||
|
* @return ?MediaType Type of file, or null.
|
||||||
|
*/
|
||||||
public function getLocalMediaType(): ?MediaType {
|
public function getLocalMediaType(): ?MediaType {
|
||||||
return MediaType::fromPath($this->localFileName);
|
return MediaType::fromPath($this->localFileName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the suggested name for the uploaded file.
|
||||||
|
*
|
||||||
|
* @return string Suggested name for the file.
|
||||||
|
*/
|
||||||
public function getSuggestedFileName(): string {
|
public function getSuggestedFileName(): string {
|
||||||
return $this->suggestedFileName;
|
return $this->suggestedFileName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the suggested media type for the uploaded file.
|
||||||
|
*
|
||||||
|
* @return MediaType Suggested type for the file.
|
||||||
|
*/
|
||||||
public function getSuggestedMediaType(): MediaType {
|
public function getSuggestedMediaType(): MediaType {
|
||||||
return $this->suggestedMediaType;
|
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 {
|
public function hasMoved(): bool {
|
||||||
return $this->hasMoved;
|
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 {
|
public function getStream(): Stream {
|
||||||
if($this->stream === null) {
|
if($this->stream === null) {
|
||||||
if($this->errorCode !== UPLOAD_ERR_OK)
|
if($this->errorCode !== UPLOAD_ERR_OK)
|
||||||
|
@ -79,6 +130,15 @@ class HttpUploadedFile implements ICloseable {
|
||||||
return $this->stream;
|
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 {
|
public function moveTo(string $path): void {
|
||||||
if($this->hasMoved)
|
if($this->hasMoved)
|
||||||
throw new RuntimeException('This uploaded file has already been moved.');
|
throw new RuntimeException('This uploaded file has already been moved.');
|
||||||
|
@ -110,6 +170,11 @@ class HttpUploadedFile implements ICloseable {
|
||||||
$this->close();
|
$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 {
|
public static function createFromFILE(array $file): self {
|
||||||
return new HttpUploadedFile(
|
return new HttpUploadedFile(
|
||||||
$file['error'] ?? UPLOAD_ERR_NO_FILE,
|
$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 {
|
public static function createFromFILES(array $files): array {
|
||||||
if(empty($files))
|
if(empty($files))
|
||||||
return [];
|
return [];
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpDelete.php
|
// HttpDelete.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpDelete extends HttpRoute {
|
class HttpDelete extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('DELETE', $path);
|
parent::__construct('DELETE', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpGet.php
|
// HttpGet.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpGet extends HttpRoute {
|
class HttpGet extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('GET', $path);
|
parent::__construct('GET', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpOptions.php
|
// HttpOptions.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpOptions extends HttpRoute {
|
class HttpOptions extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('OPTIONS', $path);
|
parent::__construct('OPTIONS', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpPatch.php
|
// HttpPatch.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpPatch extends HttpRoute {
|
class HttpPatch extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('PATCH', $path);
|
parent::__construct('PATCH', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpPost.php
|
// HttpPost.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpPost extends HttpRoute {
|
class HttpPost extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('POST', $path);
|
parent::__construct('POST', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpPut.php
|
// HttpPut.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -12,6 +12,9 @@ use Attribute;
|
||||||
*/
|
*/
|
||||||
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
|
||||||
class HttpPut extends HttpRoute {
|
class HttpPut extends HttpRoute {
|
||||||
|
/**
|
||||||
|
* @param string $path Path this route represents.
|
||||||
|
*/
|
||||||
public function __construct(string $path) {
|
public function __construct(string $path) {
|
||||||
parent::__construct('PUT', $path);
|
parent::__construct('PUT', $path);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// HttpRouter.php
|
// HttpRouter.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
@ -26,6 +26,11 @@ class HttpRouter implements IRouter {
|
||||||
|
|
||||||
private IErrorHandler $errorHandler;
|
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(
|
public function __construct(
|
||||||
string $charSet = '',
|
string $charSet = '',
|
||||||
IErrorHandler|string $errorHandler = 'html',
|
IErrorHandler|string $errorHandler = 'html',
|
||||||
|
@ -38,16 +43,31 @@ class HttpRouter implements IRouter {
|
||||||
$this->registerDefaultContentHandlers();
|
$this->registerDefaultContentHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the normalised name of the preferred character set.
|
||||||
|
*
|
||||||
|
* @return string Normalised character set name.
|
||||||
|
*/
|
||||||
public function getCharSet(): string {
|
public function getCharSet(): string {
|
||||||
if($this->defaultCharSet === '')
|
if($this->defaultCharSet === '')
|
||||||
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
|
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
|
||||||
return $this->defaultCharSet;
|
return $this->defaultCharSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the error handler instance.
|
||||||
|
*
|
||||||
|
* @return IErrorHandler The error handler.
|
||||||
|
*/
|
||||||
public function getErrorHandler(): IErrorHandler {
|
public function getErrorHandler(): IErrorHandler {
|
||||||
return $this->errorHandler;
|
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 {
|
public function setErrorHandler(IErrorHandler|string $handler): void {
|
||||||
if($handler instanceof IErrorHandler)
|
if($handler instanceof IErrorHandler)
|
||||||
$this->errorHandler = $handler;
|
$this->errorHandler = $handler;
|
||||||
|
@ -57,25 +77,45 @@ class HttpRouter implements IRouter {
|
||||||
$this->setPlainErrorHandler();
|
$this->setPlainErrorHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the error handler to the basic HTML one.
|
||||||
|
*/
|
||||||
public function setHTMLErrorHandler(): void {
|
public function setHTMLErrorHandler(): void {
|
||||||
$this->errorHandler = new HtmlErrorHandler;
|
$this->errorHandler = new HtmlErrorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the error handler to the plain text one.
|
||||||
|
*/
|
||||||
public function setPlainErrorHandler(): void {
|
public function setPlainErrorHandler(): void {
|
||||||
$this->errorHandler = new PlainErrorHandler;
|
$this->errorHandler = new PlainErrorHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a message body content handler.
|
||||||
|
*
|
||||||
|
* @param IContentHandler $contentHandler Content handler to register.
|
||||||
|
*/
|
||||||
public function registerContentHandler(IContentHandler $contentHandler): void {
|
public function registerContentHandler(IContentHandler $contentHandler): void {
|
||||||
if(!in_array($contentHandler, $this->contentHandlers))
|
if(!in_array($contentHandler, $this->contentHandlers))
|
||||||
$this->contentHandlers[] = $contentHandler;
|
$this->contentHandlers[] = $contentHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the default content handlers.
|
||||||
|
*/
|
||||||
public function registerDefaultContentHandlers(): void {
|
public function registerDefaultContentHandlers(): void {
|
||||||
$this->registerContentHandler(new StreamContentHandler);
|
$this->registerContentHandler(new StreamContentHandler);
|
||||||
$this->registerContentHandler(new JsonContentHandler);
|
$this->registerContentHandler(new JsonContentHandler);
|
||||||
$this->registerContentHandler(new BencodeContentHandler);
|
$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 {
|
public function scopeTo(string $prefix): IRouter {
|
||||||
return new ScopedRouter($this, $prefix);
|
return new ScopedRouter($this, $prefix);
|
||||||
}
|
}
|
||||||
|
@ -92,6 +132,12 @@ class HttpRouter implements IRouter {
|
||||||
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
|
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 {
|
public function use(string $path, callable $handler): void {
|
||||||
$this->middlewares[] = $mwInfo = new stdClass;
|
$this->middlewares[] = $mwInfo = new stdClass;
|
||||||
$mwInfo->handler = $handler;
|
$mwInfo->handler = $handler;
|
||||||
|
@ -105,6 +151,15 @@ class HttpRouter implements IRouter {
|
||||||
$mwInfo->prefix = $path;
|
$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 {
|
public function add(string $method, string $path, callable $handler): void {
|
||||||
if($method === '')
|
if($method === '')
|
||||||
throw new InvalidArgumentException('$method may not be empty');
|
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 {
|
public function resolve(string $method, string $path): ResolvedRouteInfo {
|
||||||
if(str_ends_with($path, '/'))
|
if(str_ends_with($path, '/'))
|
||||||
$path = substr($path, 0, -1);
|
$path = substr($path, 0, -1);
|
||||||
|
@ -178,6 +240,12 @@ class HttpRouter implements IRouter {
|
||||||
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
|
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 {
|
public function dispatch(?HttpRequest $request = null, array $args = []): void {
|
||||||
$request ??= HttpRequest::fromRequest();
|
$request ??= HttpRequest::fromRequest();
|
||||||
$response = new HttpResponseBuilder;
|
$response = new HttpResponseBuilder;
|
||||||
|
@ -232,11 +300,24 @@ class HttpRouter implements IRouter {
|
||||||
self::output($response->toResponse(), $request->getMethod() !== 'HEAD');
|
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 {
|
public function writeErrorPage(HttpResponseBuilder $response, HttpRequest $request, int $statusCode): void {
|
||||||
$response->setStatusCode($statusCode);
|
$response->setStatusCode($statusCode);
|
||||||
$this->errorHandler->handle($response, $request, $response->getStatusCode(), $response->getStatusText());
|
$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 {
|
public static function output(HttpResponse $response, bool $includeBody): void {
|
||||||
header(sprintf(
|
header(sprintf(
|
||||||
'HTTP/%s %03d %s',
|
'HTTP/%s %03d %s',
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
<?php
|
<?php
|
||||||
// ResolvedRouteInfo.php
|
// ResolvedRouteInfo.php
|
||||||
// Created: 2024-03-28
|
// Created: 2024-03-28
|
||||||
// Updated: 2024-03-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Http\Routing;
|
namespace Index\Http\Routing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a resolved route.
|
||||||
|
*/
|
||||||
class ResolvedRouteInfo {
|
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(
|
public function __construct(
|
||||||
private array $middlewares,
|
private array $middlewares,
|
||||||
private array $supportedMethods,
|
private array $supportedMethods,
|
||||||
|
@ -13,6 +22,12 @@ class ResolvedRouteInfo {
|
||||||
private array $args,
|
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 {
|
public function runMiddleware(array $args): mixed {
|
||||||
foreach($this->middlewares as $middleware) {
|
foreach($this->middlewares as $middleware) {
|
||||||
$result = $middleware[0](...array_merge($args, $middleware[1]));
|
$result = $middleware[0](...array_merge($args, $middleware[1]));
|
||||||
|
@ -23,18 +38,39 @@ class ResolvedRouteInfo {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this route has a handler.
|
||||||
|
*
|
||||||
|
* @return bool true if it does.
|
||||||
|
*/
|
||||||
public function hasHandler(): bool {
|
public function hasHandler(): bool {
|
||||||
return $this->handler !== null;
|
return $this->handler !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this route supports other HTTP request methods.
|
||||||
|
*
|
||||||
|
* @return bool true if it does.
|
||||||
|
*/
|
||||||
public function hasOtherMethods(): bool {
|
public function hasOtherMethods(): bool {
|
||||||
return !empty($this->supportedMethods);
|
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 {
|
public function getSupportedMethods(): array {
|
||||||
return $this->supportedMethods;
|
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 {
|
public function dispatch(array $args): mixed {
|
||||||
return ($this->handler)(...array_merge($args, $this->args));
|
return ($this->handler)(...array_merge($args, $this->args));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,121 @@
|
||||||
<?php
|
<?php
|
||||||
// FileStream.php
|
// FileStream.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Stream representing a file.
|
||||||
|
*/
|
||||||
class FileStream extends GenericStream {
|
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
|
* Open file for reading, throw if not exists.
|
||||||
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
|
* @var string
|
||||||
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 OPEN_READ = 'rb';
|
||||||
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
|
|
||||||
|
|
||||||
public const LOCK_NONE = 0;
|
/**
|
||||||
public const LOCK_READ = LOCK_SH;
|
* Open file for reading and writing, throw if not exist.
|
||||||
public const LOCK_WRITE = LOCK_EX;
|
*
|
||||||
|
* @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;
|
public const LOCK_NON_BLOCKING = LOCK_NB;
|
||||||
|
|
||||||
private int $lock;
|
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) {
|
public function __construct(string $path, string $mode, int $lock) {
|
||||||
$this->lock = $lock;
|
$this->lock = $lock;
|
||||||
|
|
||||||
|
@ -52,33 +142,112 @@ class FileStream extends GenericStream {
|
||||||
parent::close();
|
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 {
|
public static function openRead(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::OPEN_READ, $lock);
|
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 {
|
public static function openReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::OPEN_READ_WRITE, $lock);
|
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 {
|
public static function newWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::NEW_WRITE, $lock);
|
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 {
|
public static function newReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::NEW_READ_WRITE, $lock);
|
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 {
|
public static function appendWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::APPEND_WRITE, $lock);
|
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 {
|
public static function appendReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::APPEND_READ_WRITE, $lock);
|
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 {
|
public static function createWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::CREATE_WRITE, $lock);
|
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 {
|
public static function createReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::CREATE_READ_WRITE, $lock);
|
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 {
|
public static function openOrCreateWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::OPEN_OR_CREATE_WRITE, $lock);
|
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 {
|
public static function openOrCreateReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
|
||||||
return new FileStream($path, self::OPEN_OR_CREATE_READ_WRITE, $lock);
|
return new FileStream($path, self::OPEN_OR_CREATE_READ_WRITE, $lock);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,31 @@
|
||||||
<?php
|
<?php
|
||||||
// GenericStream.php
|
// GenericStream.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2023-07-17
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents any stream that can be handled using the C-like f* functions.
|
||||||
|
*/
|
||||||
class GenericStream extends Stream {
|
class GenericStream extends Stream {
|
||||||
|
/**
|
||||||
|
* Readable file modes.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
public const READABLE = [
|
public const READABLE = [
|
||||||
'r', 'rb', 'r+', 'r+b', 'w+', 'w+b',
|
'r', 'rb', 'r+', 'r+b', 'w+', 'w+b',
|
||||||
'a+', 'a+b', 'x+', 'x+b', 'c+', 'c+b',
|
'a+', 'a+b', 'x+', 'x+b', 'c+', 'c+b',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writeable file modes.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
public const WRITEABLE = [
|
public const WRITEABLE = [
|
||||||
'r+', 'r+b', 'w', 'wb', 'w+', 'w+b',
|
'r+', 'r+b', 'w', 'wb', 'w+', 'w+b',
|
||||||
'a', 'ab', 'a+', 'a+b', 'x', 'xb',
|
'a', 'ab', 'a+', 'a+b', 'x', 'xb',
|
||||||
|
@ -24,6 +38,9 @@ class GenericStream extends Stream {
|
||||||
private bool $canWrite;
|
private bool $canWrite;
|
||||||
private bool $canSeek;
|
private bool $canSeek;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param resource $stream Resource that this stream represents.
|
||||||
|
*/
|
||||||
public function __construct($stream) {
|
public function __construct($stream) {
|
||||||
if(!is_resource($stream) || get_resource_type($stream) !== 'stream')
|
if(!is_resource($stream) || get_resource_type($stream) !== 'stream')
|
||||||
throw new InvalidArgumentException('$stream is not a valid stream resource.');
|
throw new InvalidArgumentException('$stream is not a valid stream resource.');
|
||||||
|
@ -35,6 +52,11 @@ class GenericStream extends Stream {
|
||||||
$this->canSeek = $metaData['seekable'];
|
$this->canSeek = $metaData['seekable'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the underlying resource handle.
|
||||||
|
*
|
||||||
|
* @return resource Underlying resource handle.
|
||||||
|
*/
|
||||||
public function getResource() {
|
public function getResource() {
|
||||||
return $this->stream;
|
return $this->stream;
|
||||||
}
|
}
|
||||||
|
@ -98,8 +120,8 @@ class GenericStream extends Stream {
|
||||||
return $buffer;
|
return $buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seek(int $offset, int $origin = Stream::START): int {
|
public function seek(int $offset, int $origin = Stream::START): bool {
|
||||||
return fseek($this->stream, $offset, $origin);
|
return fseek($this->stream, $offset, $origin) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function write(string $buffer, int $length = -1): void {
|
public function write(string $buffer, int $length = -1): void {
|
||||||
|
@ -123,6 +145,7 @@ class GenericStream extends Stream {
|
||||||
} catch(\Error $ex) {}
|
} catch(\Error $ex) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
public function copyTo(Stream $other): void {
|
public function copyTo(Stream $other): void {
|
||||||
if($other instanceof GenericStream) {
|
if($other instanceof GenericStream) {
|
||||||
stream_copy_to_stream($this->stream, $other->stream);
|
stream_copy_to_stream($this->stream, $other->stream);
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<?php
|
<?php
|
||||||
// IOException.php
|
// IOException.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2021-04-30
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception type for the Index\IO namespace.
|
||||||
|
*/
|
||||||
class IOException extends RuntimeException {}
|
class IOException extends RuntimeException {}
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
// MemoryStream.php
|
// MemoryStream.php
|
||||||
// Created: 2021-05-02
|
// Created: 2021-05-02
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an in-memory stream.
|
||||||
|
*/
|
||||||
class MemoryStream extends GenericStream {
|
class MemoryStream extends GenericStream {
|
||||||
public function __construct() {
|
public function __construct() {
|
||||||
parent::__construct(fopen('php://memory', 'r+b'));
|
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 {
|
public static function fromString(string $string): MemoryStream {
|
||||||
$stream = new MemoryStream;
|
$stream = new MemoryStream;
|
||||||
$stream->write($string);
|
$stream->write($string);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// NetworkStream.php
|
// NetworkStream.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
|
@ -9,7 +9,16 @@ use ErrorException;
|
||||||
use Index\Net\IPAddress;
|
use Index\Net\IPAddress;
|
||||||
use Index\Net\EndPoint;
|
use Index\Net\EndPoint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a network socket stream.
|
||||||
|
*/
|
||||||
class NetworkStream extends GenericStream {
|
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) {
|
public function __construct(string $hostname, int $port, float|null $timeout) {
|
||||||
try {
|
try {
|
||||||
$stream = fsockopen($hostname, $port, $errcode, $errmsg, $timeout);
|
$stream = fsockopen($hostname, $port, $errcode, $errmsg, $timeout);
|
||||||
|
@ -23,41 +32,118 @@ class NetworkStream extends GenericStream {
|
||||||
parent::__construct($stream);
|
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 {
|
public function setBlocking(bool $blocking): void {
|
||||||
stream_set_blocking($this->stream, $blocking);
|
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 {
|
public static function openEndPoint(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream((string)$endPoint, -1, $timeout);
|
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 {
|
public static function openEndPointSSL(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('ssl://' . ((string)$endPoint), -1, $timeout);
|
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 {
|
public static function openEndPointTLS(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('tls://' . ((string)$endPoint), -1, $timeout);
|
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 {
|
public static function openHost(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream($hostname, $port, $timeout);
|
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 {
|
public static function openHostSSL(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('ssl://' . ((string)$hostname), $port, $timeout);
|
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 {
|
public static function openHostTLS(string $hostname, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('tls://' . ((string)$hostname), $port, $timeout);
|
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 {
|
public static function openAddress(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream($address->getAddress(), $port, $timeout);
|
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 {
|
public static function openAddressSSL(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('ssl://' . $address->getAddress(), $port, $timeout);
|
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 {
|
public static function openAddressTLS(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
|
||||||
return new NetworkStream('tls://' . $address->getAddress(), $port, $timeout);
|
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 {
|
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
|
<?php
|
||||||
// ProcessStream.php
|
// ProcessStream.php
|
||||||
// Created: 2023-01-25
|
// Created: 2023-01-25
|
||||||
// Updated: 2023-01-25
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a stream to a running sub-process.
|
||||||
|
*/
|
||||||
class ProcessStream extends Stream {
|
class ProcessStream extends Stream {
|
||||||
private $handle;
|
private $handle;
|
||||||
private bool $canRead;
|
private bool $canRead;
|
||||||
private bool $canWrite;
|
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) {
|
public function __construct(string $command, string $mode) {
|
||||||
$this->handle = popen($command, $mode);
|
$this->handle = popen($command, $mode);
|
||||||
$this->canRead = strpos($mode, 'r') !== false;
|
$this->canRead = strpos($mode, 'r') !== false;
|
||||||
|
@ -71,7 +79,7 @@ class ProcessStream extends Stream {
|
||||||
return $buffer;
|
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.');
|
throw new IOException('Cannot seek ProcessStream.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,37 +1,113 @@
|
||||||
<?php
|
<?php
|
||||||
// Stream.php
|
// Stream.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use Index\ICloseable;
|
use Index\ICloseable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic data stream.
|
||||||
|
*/
|
||||||
abstract class Stream implements Stringable, ICloseable {
|
abstract class Stream implements Stringable, ICloseable {
|
||||||
|
/**
|
||||||
|
* Place the cursor relative to the start of the file.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const START = SEEK_SET;
|
public const START = SEEK_SET;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place the cursor relative to the current position of the cursor.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const CURRENT = SEEK_CUR;
|
public const CURRENT = SEEK_CUR;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Place the cursor relative to the end of the file.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const END = SEEK_END;
|
public const END = SEEK_END;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current cursor position.
|
||||||
|
*
|
||||||
|
* @return int Cursor position.
|
||||||
|
*/
|
||||||
abstract public function getPosition(): int;
|
abstract public function getPosition(): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current length of the stream.
|
||||||
|
*
|
||||||
|
* @return int Stream length.
|
||||||
|
*/
|
||||||
abstract public function getLength(): int;
|
abstract public function getLength(): int;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the length of the stream.
|
||||||
|
*
|
||||||
|
* @param int $length Desired stream length.
|
||||||
|
*/
|
||||||
abstract public function setLength(int $length): void;
|
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;
|
abstract public function canRead(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this stream is writeable.
|
||||||
|
*
|
||||||
|
* @return bool true if the stream is writeable.
|
||||||
|
*/
|
||||||
abstract public function canWrite(): bool;
|
abstract public function canWrite(): bool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether this stream is seekable.
|
||||||
|
*
|
||||||
|
* @return bool true if the stream is seekable.
|
||||||
|
*/
|
||||||
abstract public function canSeek(): bool;
|
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;
|
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;
|
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;
|
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;
|
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 {
|
public function readByte(): int {
|
||||||
$char = $this->readChar();
|
$char = $this->readChar();
|
||||||
if($char === null)
|
if($char === null)
|
||||||
|
@ -39,25 +115,72 @@ abstract class Stream implements Stringable, ICloseable {
|
||||||
return ord($char);
|
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;
|
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 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;
|
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 {
|
public function writeLine(string $line): void {
|
||||||
$this->write($line);
|
$this->write($line);
|
||||||
$this->write(PHP_EOL);
|
$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;
|
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 {
|
public function writeByte(int $byte): void {
|
||||||
$this->writeChar(chr($byte & 0xFF));
|
$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 {
|
public function copyTo(Stream $other): void {
|
||||||
if($this->canSeek())
|
if($this->canSeek())
|
||||||
$this->seek(0);
|
$this->seek(0);
|
||||||
|
@ -66,6 +189,9 @@ abstract class Stream implements Stringable, ICloseable {
|
||||||
$other->write($this->read(8192));
|
$other->write($this->read(8192));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force write of all buffered output to the stream.
|
||||||
|
*/
|
||||||
abstract public function flush(): void;
|
abstract public function flush(): void;
|
||||||
|
|
||||||
abstract public function close(): void;
|
abstract public function close(): void;
|
||||||
|
|
|
@ -1,13 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
// TempFileStream.php
|
// TempFileStream.php
|
||||||
// Created: 2021-05-02
|
// Created: 2021-05-02
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\IO;
|
namespace Index\IO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold.
|
||||||
|
*/
|
||||||
class TempFileStream extends GenericStream {
|
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;
|
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) {
|
public function __construct(?string $body = null, int $maxMemory = self::MAX_MEMORY) {
|
||||||
parent::__construct(fopen("php://temp/maxmemory:{$maxMemory}", 'r+b'));
|
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 {
|
public static function fromString(string $string): TempFileStream {
|
||||||
return new TempFileStream($string);
|
return new TempFileStream($string);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
<?php
|
<?php
|
||||||
// MediaType.php
|
// MediaType.php
|
||||||
// Created: 2022-02-10
|
// Created: 2022-02-10
|
||||||
// Updated: 2023-07-22
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index;
|
namespace Index;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implements a structure for representing and comparing media/mime types.
|
||||||
|
*/
|
||||||
class MediaType implements Stringable, IComparable, IEquatable {
|
class MediaType implements Stringable, IComparable, IEquatable {
|
||||||
private string $category = '';
|
private string $category = '';
|
||||||
private string $kind = '';
|
private string $kind = '';
|
||||||
|
|
|
@ -1,17 +1,24 @@
|
||||||
<?php
|
<?php
|
||||||
// DnsEndPoint.php
|
// DnsEndPoint.php
|
||||||
// Created: 2021-05-02
|
// Created: 2021-05-02
|
||||||
// Updated: 2023-01-01
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Stringable;
|
|
||||||
|
|
||||||
class DnsEndPoint extends EndPoint implements Stringable {
|
/**
|
||||||
|
* A DNS end point.
|
||||||
|
*/
|
||||||
|
class DnsEndPoint extends EndPoint {
|
||||||
private string $host;
|
private string $host;
|
||||||
private int $port;
|
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) {
|
public function __construct(string $host, int $port) {
|
||||||
if($port < 0 || $port > 0xFFFF)
|
if($port < 0 || $port > 0xFFFF)
|
||||||
throw new InvalidArgumentException('$port is not a valid port number.');
|
throw new InvalidArgumentException('$port is not a valid port number.');
|
||||||
|
@ -19,14 +26,29 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
||||||
$this->port = $port;
|
$this->port = $port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves host name.
|
||||||
|
*
|
||||||
|
* @return string Host name.
|
||||||
|
*/
|
||||||
public function getHost(): string {
|
public function getHost(): string {
|
||||||
return $this->host;
|
return $this->host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a port is specified.
|
||||||
|
*
|
||||||
|
* @return bool true if the port number is greater than 0.
|
||||||
|
*/
|
||||||
public function hasPort(): bool {
|
public function hasPort(): bool {
|
||||||
return $this->port > 0;
|
return $this->port > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves port number.
|
||||||
|
*
|
||||||
|
* @return int Network port number.
|
||||||
|
*/
|
||||||
public function getPort(): int {
|
public function getPort(): int {
|
||||||
return $this->port;
|
return $this->port;
|
||||||
}
|
}
|
||||||
|
@ -34,6 +56,7 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
||||||
public function __serialize(): array {
|
public function __serialize(): array {
|
||||||
return [$this->host, $this->port];
|
return [$this->host, $this->port];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __unserialize(array $serialized): void {
|
public function __unserialize(array $serialized): void {
|
||||||
$this->host = $serialized[0];
|
$this->host = $serialized[0];
|
||||||
$this->port = $serialized[1];
|
$this->port = $serialized[1];
|
||||||
|
@ -45,11 +68,18 @@ class DnsEndPoint extends EndPoint implements Stringable {
|
||||||
&& $this->host === $other->host;
|
&& $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);
|
$parts = explode(':', $string);
|
||||||
if(count($parts) < 2)
|
if(count($parts) < 2)
|
||||||
throw new InvalidArgumentException('$string is not a valid host and port combo.');
|
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 {
|
public function __toString(): string {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// EndPoint.php
|
// EndPoint.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2022-02-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
|
@ -10,6 +10,9 @@ use JsonSerializable;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use Index\IEquatable;
|
use Index\IEquatable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a generic network end point.
|
||||||
|
*/
|
||||||
abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
|
abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
|
||||||
abstract public function equals(mixed $other): bool;
|
abstract public function equals(mixed $other): bool;
|
||||||
abstract public function __toString(): string;
|
abstract public function __toString(): string;
|
||||||
|
@ -18,29 +21,36 @@ abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
|
||||||
return (string)$this;
|
return (string)$this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parse(string $endPoint): EndPoint {
|
/**
|
||||||
if($endPoint === '')
|
* Attempts to parse a string into a known end point object.
|
||||||
throw new InvalidArgumentException('$endPoint is not a valid endpoint string.');
|
*
|
||||||
|
* @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:'))
|
if($firstChar === '/' || str_starts_with($string, 'unix:'))
|
||||||
$endPoint = UnixEndPoint::parse($endPoint);
|
$endPoint = UnixEndPoint::parse($string);
|
||||||
elseif($firstChar === '[') { // IPv6
|
elseif($firstChar === '[') { // IPv6
|
||||||
if(str_contains($endPoint, ']:'))
|
if(str_contains($string, ']:'))
|
||||||
$endPoint = IPEndPoint::parse($endPoint);
|
$endPoint = IPEndPoint::parse($string);
|
||||||
else
|
else
|
||||||
$endPoint = new IPEndPoint(IPAddress::parse(trim($endPoint, '[]')), 0);
|
$endPoint = new IPEndPoint(IPAddress::parse(trim($string, '[]')), 0);
|
||||||
} elseif(is_numeric($firstChar)) { // IPv4
|
} elseif(is_numeric($firstChar)) { // IPv4
|
||||||
if(str_contains($endPoint, ':'))
|
if(str_contains($string, ':'))
|
||||||
$endPoint = IPEndPoint::parse($endPoint);
|
$endPoint = IPEndPoint::parse($string);
|
||||||
else
|
else
|
||||||
$endPoint = new IPEndPoint(IPAddress::parse($endPoint), 0);
|
$endPoint = new IPEndPoint(IPAddress::parse($string), 0);
|
||||||
} else { // DNS
|
} else { // DNS
|
||||||
if(str_contains($endPoint, ':'))
|
if(str_contains($string, ':'))
|
||||||
$endPoint = DnsEndPoint::parse($endPoint);
|
$endPoint = DnsEndPoint::parse($string);
|
||||||
else
|
else
|
||||||
$endPoint = new DnsEndPoint($endPoint, 0);
|
$endPoint = new DnsEndPoint($string, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $endPoint;
|
return $endPoint;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// IPAddress.php
|
// IPAddress.php
|
||||||
// Created: 2021-04-26
|
// Created: 2021-04-26
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
|
@ -10,33 +10,89 @@ use JsonSerializable;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use Index\IEquatable;
|
use Index\IEquatable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an IP address.
|
||||||
|
*/
|
||||||
final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
||||||
|
/**
|
||||||
|
* Unknown IP version.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const UNKNOWN = 0;
|
public const UNKNOWN = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPv4
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const V4 = 4;
|
public const V4 = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IPv6
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
public const V6 = 6;
|
public const V6 = 6;
|
||||||
|
|
||||||
private string $raw;
|
private string $raw;
|
||||||
private int $width;
|
private int $width;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $raw Raw IP address data.
|
||||||
|
*/
|
||||||
public function __construct(string $raw) {
|
public function __construct(string $raw) {
|
||||||
$this->raw = $raw;
|
$this->raw = $raw;
|
||||||
$this->width = strlen($raw);
|
$this->width = strlen($raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get raw address bytes.
|
||||||
|
*
|
||||||
|
* @return string Raw address bytes.
|
||||||
|
*/
|
||||||
public function getRaw(): string {
|
public function getRaw(): string {
|
||||||
return $this->raw;
|
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 {
|
public function getWidth(): int {
|
||||||
return $this->width;
|
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 {
|
public function getAddress(): string {
|
||||||
return sprintf($this->isV6() ? '[%s]' : '%s', inet_ntop($this->raw));
|
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 {
|
public function getCleanAddress(): string {
|
||||||
return inet_ntop($this->raw);
|
return inet_ntop($this->raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets IP version this address is for.
|
||||||
|
*
|
||||||
|
* @return int IP version.
|
||||||
|
*/
|
||||||
public function getVersion(): int {
|
public function getVersion(): int {
|
||||||
if($this->isV4())
|
if($this->isV4())
|
||||||
return self::V4;
|
return self::V4;
|
||||||
|
@ -45,9 +101,20 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
||||||
return self::UNKNOWN;
|
return self::UNKNOWN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether this is an IPv4 address.
|
||||||
|
*
|
||||||
|
* @return bool true if IPv4.
|
||||||
|
*/
|
||||||
public function isV4(): bool {
|
public function isV4(): bool {
|
||||||
return $this->width === 4;
|
return $this->width === 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether this is an IPv6 address.
|
||||||
|
*
|
||||||
|
* @return bool true if IPv6.
|
||||||
|
*/
|
||||||
public function isV6(): bool {
|
public function isV6(): bool {
|
||||||
return $this->width === 16;
|
return $this->width === 16;
|
||||||
}
|
}
|
||||||
|
@ -60,6 +127,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
||||||
return $other instanceof IPAddress && $this->getRaw() === $other->getRaw();
|
return $other instanceof IPAddress && $this->getRaw() === $other->getRaw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[\Override]
|
||||||
public function jsonSerialize(): mixed {
|
public function jsonSerialize(): mixed {
|
||||||
return inet_ntop($this->raw);
|
return inet_ntop($this->raw);
|
||||||
}
|
}
|
||||||
|
@ -67,15 +135,23 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
|
||||||
public function __serialize(): array {
|
public function __serialize(): array {
|
||||||
return [$this->raw];
|
return [$this->raw];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __unserialize(array $serialized): void {
|
public function __unserialize(array $serialized): void {
|
||||||
$this->raw = $serialized[0];
|
$this->raw = $serialized[0];
|
||||||
$this->width = strlen($this->raw);
|
$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)
|
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);
|
return new static($parsed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// IPAddressRange.php
|
// IPAddressRange.php
|
||||||
// Created: 2021-04-26
|
// Created: 2021-04-26
|
||||||
// Updated: 2023-01-01
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
|
@ -10,23 +10,45 @@ use JsonSerializable;
|
||||||
use Stringable;
|
use Stringable;
|
||||||
use Index\IEquatable;
|
use Index\IEquatable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a CIDR range of IP addresses.
|
||||||
|
*/
|
||||||
final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
||||||
private IPAddress $base;
|
private IPAddress $base;
|
||||||
private int $mask;
|
private int $mask;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IPAddress $base Base IP address.
|
||||||
|
* @param int $mask CIDR mask.
|
||||||
|
*/
|
||||||
public function __construct(IPAddress $base, int $mask) {
|
public function __construct(IPAddress $base, int $mask) {
|
||||||
$this->base = $base;
|
$this->base = $base;
|
||||||
$this->mask = $mask;
|
$this->mask = $mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the base IP address.
|
||||||
|
*
|
||||||
|
* @return IPAddress Base IP address.
|
||||||
|
*/
|
||||||
public function getBaseAddress(): IPAddress {
|
public function getBaseAddress(): IPAddress {
|
||||||
return $this->base;
|
return $this->base;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the CIDR mask.
|
||||||
|
*
|
||||||
|
* @return int CIDR mask.
|
||||||
|
*/
|
||||||
public function getMask(): int {
|
public function getMask(): int {
|
||||||
return $this->mask;
|
return $this->mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves full CIDR representation of this range.
|
||||||
|
*
|
||||||
|
* @return string CIDR string.
|
||||||
|
*/
|
||||||
public function getCIDR(): string {
|
public function getCIDR(): string {
|
||||||
return ((string)$this->base) . '/' . $this->getMask();
|
return ((string)$this->base) . '/' . $this->getMask();
|
||||||
}
|
}
|
||||||
|
@ -41,6 +63,12 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
||||||
&& $this->base->equals($other->base);
|
&& $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 {
|
public function match(IPAddress $address): bool {
|
||||||
$width = $this->base->getWidth();
|
$width = $this->base->getWidth();
|
||||||
if($address->getWidth() !== $width)
|
if($address->getWidth() !== $width)
|
||||||
|
@ -83,15 +111,23 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
|
||||||
'm' => $this->mask,
|
'm' => $this->mask,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __unserialize(array $serialized): void {
|
public function __unserialize(array $serialized): void {
|
||||||
$this->base = new IPAddress($serialized['b']);
|
$this->base = new IPAddress($serialized['b']);
|
||||||
$this->mask = $serialized['m'];
|
$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]))
|
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]);
|
return new static(IPAddress::parse($parts[0]), (int)$parts[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
// IPEndPoint.php
|
// IPEndPoint.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2023-01-01
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use Index\XString;
|
use Index\XString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an IP address end point.
|
||||||
|
*/
|
||||||
class IPEndPoint extends EndPoint {
|
class IPEndPoint extends EndPoint {
|
||||||
private IPAddress $address;
|
private IPAddress $address;
|
||||||
private int $port;
|
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) {
|
public function __construct(IPAddress $address, int $port) {
|
||||||
if($port < 0 || $port > 0xFFFF)
|
if($port < 0 || $port > 0xFFFF)
|
||||||
throw new InvalidArgumentException('$port is not a valid port number.');
|
throw new InvalidArgumentException('$port is not a valid port number.');
|
||||||
|
@ -19,14 +27,29 @@ class IPEndPoint extends EndPoint {
|
||||||
$this->port = $port;
|
$this->port = $port;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IP address.
|
||||||
|
*
|
||||||
|
* @return IPAddress IP address.
|
||||||
|
*/
|
||||||
public function getAddress(): IPAddress {
|
public function getAddress(): IPAddress {
|
||||||
return $this->address;
|
return $this->address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a port is specified.
|
||||||
|
*
|
||||||
|
* @return bool true if the port number is greater than 0.
|
||||||
|
*/
|
||||||
public function hasPort(): bool {
|
public function hasPort(): bool {
|
||||||
return $this->port > 0;
|
return $this->port > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves port number.
|
||||||
|
*
|
||||||
|
* @return int Network port number.
|
||||||
|
*/
|
||||||
public function getPort(): int {
|
public function getPort(): int {
|
||||||
return $this->port;
|
return $this->port;
|
||||||
}
|
}
|
||||||
|
@ -34,6 +57,7 @@ class IPEndPoint extends EndPoint {
|
||||||
public function __serialize(): array {
|
public function __serialize(): array {
|
||||||
return [$this->address, $this->port];
|
return [$this->address, $this->port];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __unserialize(array $serialized): void {
|
public function __unserialize(array $serialized): void {
|
||||||
$this->address = $serialized[0];
|
$this->address = $serialized[0];
|
||||||
$this->port = $serialized[1];
|
$this->port = $serialized[1];
|
||||||
|
@ -45,7 +69,14 @@ class IPEndPoint extends EndPoint {
|
||||||
&& $this->address->equals($other->address);
|
&& $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
|
if(str_starts_with($string, '[')) { // IPv6
|
||||||
$closing = strpos($string, ']');
|
$closing = strpos($string, ']');
|
||||||
if($closing === false)
|
if($closing === false)
|
||||||
|
|
|
@ -1,17 +1,28 @@
|
||||||
<?php
|
<?php
|
||||||
// UnixEndPoint.php
|
// UnixEndPoint.php
|
||||||
// Created: 2021-04-30
|
// Created: 2021-04-30
|
||||||
// Updated: 2022-02-27
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Net;
|
namespace Index\Net;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a UNIX socket path end point.
|
||||||
|
*/
|
||||||
class UnixEndPoint extends EndPoint {
|
class UnixEndPoint extends EndPoint {
|
||||||
private string $path;
|
private string $path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $socketPath Path to the socket file.
|
||||||
|
*/
|
||||||
public function __construct(string $socketPath) {
|
public function __construct(string $socketPath) {
|
||||||
$this->path = $socketPath;
|
$this->path = $socketPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets path to the socket file.
|
||||||
|
*
|
||||||
|
* @return string Path to the socket file.
|
||||||
|
*/
|
||||||
public function getPath(): string {
|
public function getPath(): string {
|
||||||
return $this->path;
|
return $this->path;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +30,7 @@ class UnixEndPoint extends EndPoint {
|
||||||
public function __serialize(): array {
|
public function __serialize(): array {
|
||||||
return [$this->path];
|
return [$this->path];
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __unserialize(array $serialized): void {
|
public function __unserialize(array $serialized): void {
|
||||||
$this->path = $serialized[0];
|
$this->path = $serialized[0];
|
||||||
}
|
}
|
||||||
|
@ -28,13 +40,19 @@ class UnixEndPoint extends EndPoint {
|
||||||
&& $this->path === $other->path;
|
&& $this->path === $other->path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function parse(string $socketPath): UnixEndPoint {
|
/**
|
||||||
if(str_starts_with($socketPath, 'unix:')) {
|
* Attempts to parse a string into an UNIX end point.
|
||||||
$uri = parse_url($socketPath);
|
*
|
||||||
$socketPath = $uri['path'] ?? '';
|
* @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 {
|
public function __toString(): string {
|
||||||
|
|
|
@ -1,19 +1,38 @@
|
||||||
<?php
|
<?php
|
||||||
// PerformanceCounter.php
|
// PerformanceCounter.php
|
||||||
// Created: 2022-02-16
|
// Created: 2022-02-16
|
||||||
// Updated: 2022-02-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Performance;
|
namespace Index\Performance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a performance counter.
|
||||||
|
*/
|
||||||
final class PerformanceCounter {
|
final class PerformanceCounter {
|
||||||
|
/**
|
||||||
|
* Timer frequency in ticks per second.
|
||||||
|
*
|
||||||
|
* @return int|float Timer frequency.
|
||||||
|
*/
|
||||||
public static function getFrequency(): int|float {
|
public static function getFrequency(): int|float {
|
||||||
return 1000000;
|
return 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current ticks from the system.
|
||||||
|
*
|
||||||
|
* @return int|float Ticks count.
|
||||||
|
*/
|
||||||
public static function getTicks(): int|float {
|
public static function getTicks(): int|float {
|
||||||
return hrtime(true);
|
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 {
|
public static function getTicksSince(int|float $since): int|float {
|
||||||
return self::getTicks() - $since;
|
return self::getTicks() - $since;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// Stopwatch.php
|
// Stopwatch.php
|
||||||
// Created: 2021-04-26
|
// Created: 2021-04-26
|
||||||
// Updated: 2022-02-16
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Performance;
|
namespace Index\Performance;
|
||||||
|
|
||||||
|
@ -17,11 +17,17 @@ class Stopwatch {
|
||||||
$this->frequency = PerformanceCounter::getFrequency();
|
$this->frequency = PerformanceCounter::getFrequency();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start time measurement.
|
||||||
|
*/
|
||||||
public function start(): void {
|
public function start(): void {
|
||||||
if($this->start < 0)
|
if($this->start < 0)
|
||||||
$this->start = PerformanceCounter::getTicks();
|
$this->start = PerformanceCounter::getTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop time measurement.
|
||||||
|
*/
|
||||||
public function stop(): void {
|
public function stop(): void {
|
||||||
if($this->start < 0)
|
if($this->start < 0)
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
<?php
|
<?php
|
||||||
// TimingPoint.php
|
// TimingPoint.php
|
||||||
// Created: 2022-02-16
|
// Created: 2022-02-16
|
||||||
// Updated: 2022-02-28
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Performance;
|
namespace Index\Performance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a timing point.
|
||||||
|
*/
|
||||||
class TimingPoint {
|
class TimingPoint {
|
||||||
private int|float $timePoint;
|
private int|float $timePoint;
|
||||||
private int|float $duration;
|
private int|float $duration;
|
||||||
private string $name;
|
private string $name;
|
||||||
private string $comment;
|
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(
|
public function __construct(
|
||||||
int|float $timePoint,
|
int|float $timePoint,
|
||||||
int|float $duration,
|
int|float $duration,
|
||||||
|
@ -23,26 +32,56 @@ class TimingPoint {
|
||||||
$this->comment = $comment;
|
$this->comment = $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the starting ticks count.
|
||||||
|
*
|
||||||
|
* @return int|float Starting ticks count.
|
||||||
|
*/
|
||||||
public function getTimePointTicks(): int|float {
|
public function getTimePointTicks(): int|float {
|
||||||
return $this->timePoint;
|
return $this->timePoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the duration ticks count.
|
||||||
|
*
|
||||||
|
* @return int|float Duration ticks count.
|
||||||
|
*/
|
||||||
public function getDurationTicks(): int|float {
|
public function getDurationTicks(): int|float {
|
||||||
return $this->duration;
|
return $this->duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the duration time amount.
|
||||||
|
*
|
||||||
|
* @return float Duration time amount.
|
||||||
|
*/
|
||||||
public function getDurationTime(): float {
|
public function getDurationTime(): float {
|
||||||
return $this->duration / PerformanceCounter::getFrequency();
|
return $this->duration / PerformanceCounter::getFrequency();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets time point name.
|
||||||
|
*
|
||||||
|
* @return string Time point name.
|
||||||
|
*/
|
||||||
public function getName(): string {
|
public function getName(): string {
|
||||||
return $this->name;
|
return $this->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether time point has a comment.
|
||||||
|
*
|
||||||
|
* @return bool true if the time point has a comment.
|
||||||
|
*/
|
||||||
public function hasComment(): bool {
|
public function hasComment(): bool {
|
||||||
return $this->comment !== '';
|
return $this->comment !== '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets time point comment.
|
||||||
|
*
|
||||||
|
* @return string Time point comment.
|
||||||
|
*/
|
||||||
public function getComment(): string {
|
public function getComment(): string {
|
||||||
return $this->comment;
|
return $this->comment;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +1,79 @@
|
||||||
<?php
|
<?php
|
||||||
// Timings.php
|
// Timings.php
|
||||||
// Created: 2022-02-16
|
// Created: 2022-02-16
|
||||||
// Updated: 2022-02-16
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index\Performance;
|
namespace Index\Performance;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a Stopwatch with timing points.
|
||||||
|
*/
|
||||||
class Timings {
|
class Timings {
|
||||||
private Stopwatch $sw;
|
private Stopwatch $sw;
|
||||||
private int|float $lastLapTicks;
|
private int|float $lastLapTicks;
|
||||||
private array $laps;
|
private array $laps;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ?Stopwatch $sw Stopwatch to measure timing points from, null to start a new one.
|
||||||
|
*/
|
||||||
public function __construct(?Stopwatch $sw = null) {
|
public function __construct(?Stopwatch $sw = null) {
|
||||||
$this->sw = $sw ?? Stopwatch::startNew();
|
$this->sw = $sw ?? Stopwatch::startNew();
|
||||||
$this->lastLapTicks = $this->sw->getElapsedTicks();
|
$this->lastLapTicks = $this->sw->getElapsedTicks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the underlying stopwatch object.
|
||||||
|
*
|
||||||
|
* @return Stopwatch Stopwatch object.
|
||||||
|
*/
|
||||||
public function getStopwatch(): Stopwatch {
|
public function getStopwatch(): Stopwatch {
|
||||||
return $this->sw;
|
return $this->sw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns timing points.
|
||||||
|
*
|
||||||
|
* @return TimingPoint[] Timing points.
|
||||||
|
*/
|
||||||
public function getLaps(): array {
|
public function getLaps(): array {
|
||||||
return $this->laps;
|
return $this->laps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the underlying stopwatch.
|
||||||
|
*/
|
||||||
public function start(): void {
|
public function start(): void {
|
||||||
$this->sw->start();
|
$this->sw->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the underlying stopwatch.
|
||||||
|
*/
|
||||||
public function stop(): void {
|
public function stop(): void {
|
||||||
$this->sw->stop();
|
$this->sw->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the underlying stopwatch is running.
|
||||||
|
*
|
||||||
|
* @return bool true if it is running.
|
||||||
|
*/
|
||||||
public function isRunning(): bool {
|
public function isRunning(): bool {
|
||||||
return $this->sw->isRunning();
|
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 {
|
public function lap(string $name, string $comment = ''): void {
|
||||||
if(!ctype_alnum($name))
|
if(!ctype_alnum($name))
|
||||||
throw new InvalidArgumentException('$name must be alpha-numeric.');
|
throw new InvalidArgumentException('$name must be alphanumeric.');
|
||||||
|
|
||||||
$lapTicks = $this->sw->getElapsedTicks();
|
$lapTicks = $this->sw->getElapsedTicks();
|
||||||
$elapsed = $lapTicks - $this->lastLapTicks;
|
$elapsed = $lapTicks - $this->lastLapTicks;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// XDateTime.php
|
// XDateTime.php
|
||||||
// Created: 2024-07-31
|
// Created: 2024-07-31
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
namespace Index;
|
namespace Index;
|
||||||
|
|
||||||
|
@ -9,6 +9,9 @@ use DateTimeImmutable;
|
||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a set of DateTime related utilities.
|
||||||
|
*/
|
||||||
final class XDateTime {
|
final class XDateTime {
|
||||||
private const COMPARE_FORMAT = 'YmdHisu';
|
private const COMPARE_FORMAT = 'YmdHisu';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// BencodeTest.php
|
// BencodeTest.php
|
||||||
// Created: 2023-07-21
|
// Created: 2023-07-21
|
||||||
// Updated: 2024-07-31
|
// Updated: 2024-08-01
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ final class BencodeTest extends TestCase {
|
||||||
$this->assertArrayHasKey('private', $decoded['info']);
|
$this->assertArrayHasKey('private', $decoded['info']);
|
||||||
$this->assertEquals(1, $decoded['info']['private']);
|
$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('https://tracker.flashii.net/announce.php/meow', $decoded->announce);
|
||||||
$this->assertEquals(1689973664, $decoded->{'creation date'});
|
$this->assertEquals(1689973664, $decoded->{'creation date'});
|
||||||
|
|
Loading…
Reference in a new issue