Updated PHPDocs and some argument lists.

This commit is contained in:
flash 2024-08-01 22:16:38 +00:00
parent 81ad848c83
commit 51f97b7a67
63 changed files with 2058 additions and 166 deletions

View file

@ -1 +1 @@
0.2408.12207 0.2408.12215

View file

@ -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);
} }
} }
} }

View file

@ -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).

View file

@ -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 {}

View file

@ -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();

View file

@ -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 = [];

View file

@ -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);

View file

@ -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);

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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;
} }

View file

@ -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');
} }

View file

@ -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);
} }

View file

@ -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 {}

View file

@ -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');
} }

View file

@ -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');
} }

View file

@ -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');
} }

View file

@ -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;

View file

@ -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;
} }

View file

@ -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;

View file

@ -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;

View file

@ -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>

View file

@ -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;
} }

View file

@ -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();

View file

@ -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];
} }

View file

@ -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 '';

View file

@ -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 = [];

View file

@ -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;
} }

View file

@ -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);

View file

@ -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']);

View file

@ -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(),

View file

@ -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;
} }

View file

@ -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(),

View file

@ -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 [];

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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);
} }

View file

@ -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',

View file

@ -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));
} }

View file

@ -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);
} }

View file

@ -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);

View file

@ -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 {}

View file

@ -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);

View file

@ -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);
} }
} }

View file

@ -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.');
} }

View file

@ -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;

View file

@ -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);
} }

View file

@ -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 = '';

View file

@ -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 {

View file

@ -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;

View file

@ -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);
} }
} }

View file

@ -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]);
} }
} }

View file

@ -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)

View file

@ -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 {

View file

@ -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;
} }

View file

@ -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;

View file

@ -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;
} }

View file

@ -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;

View file

@ -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';

View file

@ -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'});