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
// Bencode.php
// Created: 2022-01-13
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Bencode;
@ -15,44 +15,57 @@ use Index\IO\TempFileStream;
* Provides a Bencode serialiser.
*/
final class Bencode {
/**
* Default maximum depth.
*
* @var int
*/
public const DEFAULT_DEPTH = 512;
public static function encode(mixed $input, int $depth = self::DEFAULT_DEPTH): string {
/**
* Returns the Bencoded representation of a value.
*
* @param mixed $value The value being encoded. Can by string, integer, array or an object.
* @param int $depth Set the maximum depth. Must be greater than zero.
* @throws RuntimeException If $depth is an invalid value.
* @return string Returns a Bencoded string.
*/
public static function encode(mixed $value, int $depth = self::DEFAULT_DEPTH): string {
if($depth < 1)
throw new RuntimeException('Maximum depth reached, structure is too dense.');
switch(gettype($input)) {
switch(gettype($value)) {
case 'string':
return sprintf('%d:%s', strlen($input), $input);
return sprintf('%d:%s', strlen($value), $value);
case 'integer':
return sprintf('i%de', $input);
return sprintf('i%de', $value);
case 'array':
if(array_is_list($input)) {
if(array_is_list($value)) {
$output = 'l';
foreach($input as $item)
foreach($value as $item)
$output .= self::encode($item, $depth - 1);
} else {
$output = 'd';
foreach($input as $key => $value) {
$output .= self::encode((string)$key, $depth - 1);
$output .= self::encode($value, $depth - 1);
foreach($value as $_key => $_value) {
$output .= self::encode((string)$_key, $depth - 1);
$output .= self::encode($_value, $depth - 1);
}
}
return $output . 'e';
case 'object':
if($input instanceof IBencodeSerialisable)
return self::encode($input->bencodeSerialise(), $depth - 1);
if($value instanceof IBencodeSerialisable)
return self::encode($value->bencodeSerialise(), $depth - 1);
$input = get_object_vars($input);
$value = get_object_vars($value);
$output = 'd';
foreach($input as $key => $value) {
$output .= self::encode((string)$key, $depth - 1);
$output .= self::encode($value, $depth - 1);
foreach($value as $_key => $_value) {
$output .= self::encode((string)$_key, $depth - 1);
$output .= self::encode($_value, $depth - 1);
}
return $output . 'e';
@ -62,28 +75,39 @@ final class Bencode {
}
}
public static function decode(mixed $input, int $depth = self::DEFAULT_DEPTH, bool $dictAsObject = false): mixed {
if(is_string($input)) {
$input = TempFileStream::fromString($input);
$input->seek(0);
} elseif(is_resource($input))
$input = new GenericStream($input);
elseif(!($input instanceof Stream))
throw new InvalidArgumentException('$input must be a string, an Index Stream or a file resource.');
/**
* Decodes a bencoded string.
*
* @param mixed $bencoded The bencoded string being decoded. Can be an Index Stream, readable resource or a string.
* @param int $depth Maximum nesting depth of the structure being decoded. The value must be greater than 0.
* @param bool $associative When true, Bencoded dictionaries will be returned as associative arrays; when false, Bencoded dictionaries will be returned as objects.
* @throws InvalidArgumentException If $bencoded string is not set to a valid type.
* @throws InvalidArgumentException If $depth is not greater than 0.
* @throws RuntimeException If the Bencoded stream is in an invalid state.
* @return mixed Returns the bencoded value as an appropriate PHP type.
*/
public static function decode(mixed $bencoded, int $depth = self::DEFAULT_DEPTH, bool $associative = true): mixed {
if(is_string($bencoded)) {
$bencoded = TempFileStream::fromString($bencoded);
$bencoded->seek(0);
} elseif(is_resource($bencoded))
$bencoded = new GenericStream($bencoded);
elseif(!($bencoded instanceof Stream))
throw new InvalidArgumentException('$bencoded must be a string, an Index Stream or a file resource.');
if($depth < 1)
throw new RuntimeException('Maximum depth reached, structure is too dense.');
throw new InvalidArgumentException('Maximum depth reached, structure is too dense.');
$char = $input->readChar();
$char = $bencoded->readChar();
if($char === null)
throw new RuntimeException('Unexpected end of stream in $input.');
throw new RuntimeException('Unexpected end of stream in $bencoded.');
switch($char) {
case 'i':
$number = '';
for(;;) {
$char = $input->readChar();
$char = $bencoded->readChar();
if($char === null)
throw new RuntimeException('Unexpected end of stream while parsing integer.');
if($char === 'e')
@ -105,13 +129,13 @@ final class Bencode {
$list = [];
for(;;) {
$char = $input->readChar();
$char = $bencoded->readChar();
if($char === null)
throw new RuntimeException('Unexpected end of stream while parsing list.');
if($char === 'e')
break;
$input->seek(-1, Stream::CURRENT);
$list[] = self::decode($input, $depth - 1, $dictAsObject);
$bencoded->seek(-1, Stream::CURRENT);
$list[] = self::decode($bencoded, $depth - 1, $associative);
}
return $list;
@ -120,18 +144,18 @@ final class Bencode {
$dict = [];
for(;;) {
$char = $input->readChar();
$char = $bencoded->readChar();
if($char === null)
throw new RuntimeException('Unexpected end of stream while parsing dictionary');
if($char === 'e')
break;
if(!ctype_digit($char))
throw new RuntimeException('Unexpected dictionary key type, expected a string.');
$input->seek(-1, Stream::CURRENT);
$dict[self::decode($input, $depth - 1, $dictAsObject)] = self::decode($input, $depth - 1, $dictAsObject);
$bencoded->seek(-1, Stream::CURRENT);
$dict[self::decode($bencoded, $depth - 1, $associative)] = self::decode($bencoded, $depth - 1, $associative);
}
if($dictAsObject)
if(!$associative)
$dict = (object)$dict;
return $dict;
@ -143,7 +167,7 @@ final class Bencode {
$length = $char;
for(;;) {
$char = $input->readChar();
$char = $bencoded->readChar();
if($char === null)
throw new RuntimeException('Unexpected end of character while parsing string length.');
if($char === ':')
@ -156,7 +180,7 @@ final class Bencode {
$length .= $char;
}
return $input->read((int)$length);
return $bencoded->read((int)$length);
}
}
}

View file

@ -1,10 +1,13 @@
<?php
// ByteFormat.php
// Created: 2023-07-05
// Updated: 2023-07-05
// Updated: 2024-08-01
namespace Index;
/**
* Implements a byte formatter for file sizes.
*/
final class ByteFormat {
/**
* Whether the default behaviour for the format function is decimal (power of 10) or not (power of 2).

View file

@ -1,13 +1,13 @@
<?php
// DataException.php
// Created: 2021-05-02
// Updated: 2021-05-12
// Updated: 2024-08-01
namespace Index\Data;
use RuntimeException;
/**
* Exception type of the Index\Data namespace.
* Exception type for the Index\Data namespace.
*/
class DataException extends RuntimeException {}

View file

@ -1,7 +1,7 @@
<?php
// DbStatementCache.php
// Created: 2023-07-21
// Updated: 2023-07-21
// Updated: 2024-08-01
namespace Index\Data;
@ -12,6 +12,9 @@ class DbStatementCache {
private IDbConnection $dbConn;
private array $stmts = [];
/**
* @param IDbConnection $dbConn Connection to use with this cache.
*/
public function __construct(IDbConnection $dbConn) {
$this->dbConn = $dbConn;
}
@ -20,6 +23,12 @@ class DbStatementCache {
return hash('xxh3', $query, true);
}
/**
* Gets a cached or creates a new IDbStatement instance.
*
* @param string $query SQL query.
* @return IDbStatement Statement representing the query.
*/
public function get(string $query): IDbStatement {
$hash = self::hash($query);
@ -32,10 +41,18 @@ class DbStatementCache {
return $this->stmts[$hash] = $this->dbConn->prepare($query);
}
/**
* Removes a cached statement from the cache.
*
* @param string $query SQL query of the statement to remove from the cache.
*/
public function remove(string $query): void {
unset($this->stmts[self::hash($query)]);
}
/**
* Closes all statement instances and resets the cache.
*/
public function clear(): void {
foreach($this->stmts as $stmt)
$stmt->close();

View file

@ -1,7 +1,7 @@
<?php
// DbMigrationManager.php
// Created: 2023-01-07
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Data\Migration;
@ -15,7 +15,15 @@ use Index\Data\IDbStatement;
use Index\Data\DbType;
use Index\Data\SQLite\SQLiteConnection;
/**
* Provides a common interface for database migrations.
*/
class DbMigrationManager {
/**
* Default table name for migrations.
*
* @var string
*/
public const DEFAULT_TABLE = 'ndx_migrations';
private const CREATE_TRACK_TABLE = 'CREATE TABLE IF NOT EXISTS %1$s (migration_name %2$s PRIMARY KEY, migration_completed %2$s NOT NULL);';
@ -41,33 +49,55 @@ EOF;
private IDbStatement $checkStmt;
private IDbStatement $insertStmt;
/**
* @param IDbConnection $conn Connection to apply to migrations to.
* @param string $tableName Name of the migration tracking table.
*/
public function __construct(
private IDbConnection $conn,
private string $tableName = self::DEFAULT_TABLE,
) {}
/**
* Creates the migration tracking table if it doesn't exist.
*/
public function createTrackingTable(): void {
// this is not ok but it works for now, there should probably be a generic type bag alias thing
// HACK: this is not ok but it works for now, there should probably be a generic type bag alias thing
$nameType = $this->conn instanceof SQLiteConnection ? 'TEXT' : 'VARCHAR(255)';
$this->conn->execute(sprintf(self::CREATE_TRACK_TABLE, $this->tableName, $nameType));
$this->conn->execute(sprintf(self::CREATE_TRACK_INDEX, $this->tableName));
}
/**
* Deletes the migration tracking table.
*/
public function destroyTrackingTable(): void {
$this->conn->execute(sprintf(self::DESTROY_TRACK_TABLE, $this->tableName));
}
/**
* Prepares statements required for migrations.
*/
public function prepareStatements(): void {
$this->checkStmt = $this->conn->prepare(sprintf(self::CHECK_STMT, $this->tableName));
$this->insertStmt = $this->conn->prepare(sprintf(self::INSERT_STMT, $this->tableName));
}
/**
* Runs all preparations required for running migrations.
*/
public function init(): void {
$this->createTrackingTable();
$this->prepareStatements();
}
/**
* Checks if a particular migration is present in the tracking table.
*
* @param string $name Name of the migration.
* @return bool true if the migration has been run, false otherwise.
*/
public function checkMigration(string $name): bool {
$this->checkStmt->reset();
$this->checkStmt->addParameter(1, $name, DbType::STRING);
@ -76,6 +106,12 @@ EOF;
return $result->next() && !$result->isNull(0);
}
/**
* Marks a migration as completed in the tracking table.
*
* @param string $name Name of the migration.
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was run, null for now.
*/
public function completeMigration(string $name, ?DateTimeInterface $dateTime = null): void {
$dateTime = XDateTime::toISO8601String($dateTime);
@ -85,10 +121,25 @@ EOF;
$this->insertStmt->execute();
}
/**
* Generates a template for a migration PHP file.
*
* @param string $name Name of the migration.
* @return string Migration template.
*/
public function template(string $name): string {
return sprintf(self::TEMPLATE, $name);
}
/**
* Generates a filename for a migration PHP file.
*
* @param string $name Name of the migration.
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
* @throws InvalidArgumentException If $name is empty.
* @throws InvalidArgumentException If $name contains invalid characters.
* @return string Migration filename.
*/
public function createFileName(string $name, ?DateTimeInterface $dateTime = null): string {
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
@ -102,6 +153,15 @@ EOF;
return $dateTime->format('Y_m_d_His_') . trim($name, '_');
}
/**
* Generates a class name for a migration PHP file.
*
* @param string $name Name of the migration.
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
* @throws InvalidArgumentException If $name is empty.
* @throws InvalidArgumentException If $name contains invalid characters.
* @return string Migration class name.
*/
public function createClassName(string $name, ?DateTimeInterface $dateTime = null): string {
if(empty($name))
throw new InvalidArgumentException('$name may not be empty.');
@ -121,6 +181,13 @@ EOF;
return $name . $dateTime->format('_Ymd_His');
}
/**
* Generate names for a PHP migration file.
*
* @param string $baseName Name of the migration.
* @param ?DateTimeInterface $dateTime Timestamp of when the migration was created, null for now.
* @return object Object containing names for a PHP migration file.
*/
public function createNames(string $baseName, ?DateTimeInterface $dateTime = null): object {
$dateTime ??= new DateTimeImmutable('now');
@ -131,6 +198,12 @@ EOF;
return $names;
}
/**
* Runs all migrations present in a migration repository. This process is irreversible!
*
* @param IDbMigrationRepo $migrations Migrations repository to fetch migrations from.
* @return array Names of migrations that have been completed.
*/
public function processMigrations(IDbMigrationRepo $migrations): array {
$migrations = $migrations->getMigrations();
$completed = [];

View file

@ -1,7 +1,7 @@
<?php
// FsDbMigrationInfo.php
// Created: 2023-01-07
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Data\Migration;
@ -12,6 +12,9 @@ class FsDbMigrationInfo implements IDbMigrationInfo {
private string $name;
private string $className;
/**
* @param string $path Filesystem path to the migration file.
*/
public function __construct(string $path) {
$this->path = $path;
$this->name = $name = pathinfo($path, PATHINFO_FILENAME);

View file

@ -1,11 +1,14 @@
<?php
// FsDbMigrationRepo.php
// Created: 2023-01-07
// Updated: 2023-01-07
// Updated: 2024-08-01
namespace Index\Data\Migration;
class FsDbMigrationRepo implements IDbMigrationRepo {
/**
* @param string $path Filesystem path to the directory containing the migration files.
*/
public function __construct(
private string $path
) {}
@ -22,6 +25,13 @@ class FsDbMigrationRepo implements IDbMigrationRepo {
return $migrations;
}
/**
* Saves a migratinon template to a file within the directory of this migration repository.
* If the repository directory does not exist, it will be created.
*
* @param string $name Name for the migration PHP file.
* @param string $body Body for the migration PHP file.
*/
public function saveMigrationTemplate(string $name, string $body): void {
if(!is_dir($this->path))
mkdir($this->path, 0777, true);

View file

@ -1,12 +1,20 @@
<?php
// IDbMigration.php
// Created: 2023-01-07
// Updated: 2023-01-07
// Updated: 2024-08-01
namespace Index\Data\Migration;
use Index\Data\IDbConnection;
/**
* Interface for migration classes to inherit.
*/
interface IDbMigration {
/**
* Runs the migration implemented by this class. This process is irreversible!
*
* @param IDbConnection $conn Database connection to execute this migration on.
*/
public function migrate(IDbConnection $conn): void;
}

View file

@ -1,14 +1,34 @@
<?php
// IDbMigrationInfo.php
// Created: 2023-01-07
// Updated: 2023-01-07
// Updated: 2024-08-01
namespace Index\Data\Migration;
use Index\Data\IDbConnection;
/**
* Information on a migration repository item.
*/
interface IDbMigrationInfo {
/**
* Returns the name of the migration.
*
* @return string Migration name.
*/
public function getName(): string;
/**
* Returns the class name of the migration.
*
* @return string Migration class name.
*/
public function getClassName(): string;
/**
* Creates an instance of the underlying migration and runs it. This process is irreversible!
*
* @param IDbConnection $conn Database connection to execute this migration on.
*/
public function migrate(IDbConnection $conn): void;
}

View file

@ -1,10 +1,18 @@
<?php
// IDbMigrationRepo.php
// Created: 2023-01-07
// Updated: 2023-01-07
// Updated: 2024-08-01
namespace Index\Data\Migration;
/**
* Represents a repository of migrations.
*/
interface IDbMigrationRepo {
/**
* Returns info on migrations contained in this repository.
*
* @return IDbMigrationInfo[] Collection of migration infos.
*/
public function getMigrations(): array;
}

View file

@ -1,23 +1,33 @@
<?php
// BencodedContent.php
// Created: 2022-02-10
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http\Content;
use Stringable;
use Index\Bencode\Bencode;
use Index\Bencode\IBencodeSerialisable;
use Index\IO\Stream;
use Index\IO\FileStream;
class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable {
/**
* Represents Bencoded body content for a HTTP message.
*/
class BencodedContent implements IHttpContent, IBencodeSerialisable {
private mixed $content;
/**
* @param mixed $content Content to be bencoded.
*/
public function __construct(mixed $content) {
$this->content = $content;
}
/**
* Retrieves unencoded content.
*
* @return mixed Content.
*/
public function getContent(): mixed {
return $this->content;
}
@ -26,6 +36,11 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
return $this->content;
}
/**
* Encodes the content.
*
* @return string Bencoded string.
*/
public function encode(): string {
return Bencode::encode($this->content);
}
@ -34,14 +49,31 @@ class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable
return $this->encode();
}
public static function fromEncoded(Stream|string $encoded): BencodedContent {
/**
* Creates an instance from encoded content.
*
* @param mixed $encoded Bencoded content.
* @return BencodedContent Instance representing the provided content.
*/
public static function fromEncoded(mixed $encoded): BencodedContent {
return new BencodedContent(Bencode::decode($encoded));
}
/**
* Creates an instance from an encoded file.
*
* @param string $path Path to the bencoded file.
* @return BencodedContent Instance representing the provided path.
*/
public static function fromFile(string $path): BencodedContent {
return self::fromEncoded(FileStream::openRead($path));
}
/**
* Creates an instance from the raw request body.
*
* @return BencodedContent Instance representing the request body.
*/
public static function fromRequest(): BencodedContent {
return self::fromFile('php://input');
}

View file

@ -1,50 +1,102 @@
<?php
// FormContent.php
// Created: 2022-02-10
// Updated: 2023-08-22
// Updated: 2024-08-01
namespace Index\Http\Content;
use RuntimeException;
use Index\Http\HttpUploadedFile;
/**
* Represents form body content for a HTTP message.
*/
class FormContent implements IHttpContent {
private array $postFields;
private array $uploadedFiles;
/**
* @param array<string, mixed> $postFields Form fields.
* @param array<string, HttpUploadedFile> $uploadedFiles Uploaded files.
*/
public function __construct(array $postFields, array $uploadedFiles) {
$this->postFields = $postFields;
$this->uploadedFiles = $uploadedFiles;
}
/**
* Retrieves a form field with filtering.
*
* @param string $name Name of the form field.
* @param int $filter A PHP filter extension filter constant.
* @param array|int $options Options for the PHP filter.
* @return mixed Value of the form field, null if not present.
*/
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
if(!isset($this->postFields[$name]))
return null;
return filter_var($this->postFields[$name] ?? null, $filter, $options);
}
/**
* Checks if a form field is present.
*
* @param string $name Name of the form field.
* @return bool true if the field is present, false if not.
*/
public function hasParam(string $name): bool {
return isset($this->postFields[$name]);
}
/**
* Gets all form fields.
*
* @return array<string, mixed> Form fields.
*/
public function getParams(): array {
return $this->postFields;
}
/**
* Gets all form fields as a query string.
*
* @param bool $spacesAsPlus true if spaces should be represented with a +, false if %20.
* @return string Query string representation of form fields.
*/
public function getParamString(bool $spacesAsPlus = false): string {
return http_build_query($this->postFields, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
}
/**
* Checks if a file upload is present.
*
* @param string $name Name of the form field.
* @return bool true if the upload is present, false if not.
*/
public function hasUploadedFile(string $name): bool {
return isset($this->uploadedFiles[$name]);
}
/**
* Retrieves a file upload.
*
* @param string $name Name of the form field.
* @throws RuntimeException If no uploaded file with form field $name is present.
* @return HttpUploadedFile Uploaded file info.
*/
public function getUploadedFile(string $name): HttpUploadedFile {
if(!isset($this->uploadedFiles[$name]))
throw new RuntimeException('No file with name $name present.');
return $this->uploadedFiles[$name];
}
/**
* Creates an instance from an array of form fields and uploaded files.
*
* @param array<string, mixed> $post Form fields.
* @param array<string, mixed> $files Uploaded files.
* @return FormContent Instance representing the request body.
*/
public static function fromRaw(array $post, array $files): FormContent {
return new FormContent(
$post,
@ -52,6 +104,11 @@ class FormContent implements IHttpContent {
);
}
/**
* Creates an instance from the $_POST and $_FILES superglobals.
*
* @return FormContent Instance representing the request body.
*/
public static function fromRequest(): FormContent {
return self::fromRaw($_POST, $_FILES);
}

View file

@ -1,11 +1,13 @@
<?php
// IHttpContent.php
// Created: 2022-02-08
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http\Content;
use Stringable;
interface IHttpContent extends Stringable {
}
/**
* Represents the body content for a HTTP message.
*/
interface IHttpContent extends Stringable {}

View file

@ -1,22 +1,32 @@
<?php
// JsonContent.php
// Created: 2022-02-10
// Updated: 2023-07-21
// Updated: 2024-08-01
namespace Index\Http\Content;
use JsonSerializable;
use Stringable;
use Index\IO\Stream;
use Index\IO\FileStream;
class JsonContent implements Stringable, IHttpContent, JsonSerializable {
/**
* Represents JSON body content for a HTTP message.
*/
class JsonContent implements IHttpContent, JsonSerializable {
private mixed $content;
/**
* @param mixed $content Content to be JSON encoded.
*/
public function __construct(mixed $content) {
$this->content = $content;
}
/**
* Retrieves unencoded content.
*
* @return mixed Content.
*/
public function getContent(): mixed {
return $this->content;
}
@ -25,6 +35,11 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
return $this->content;
}
/**
* Encodes the content.
*
* @return string JSON encoded string.
*/
public function encode(): string {
return json_encode($this->content);
}
@ -33,14 +48,31 @@ class JsonContent implements Stringable, IHttpContent, JsonSerializable {
return $this->encode();
}
public static function fromEncoded(Stream|string $encoded): JsonContent {
/**
* Creates an instance from encoded content.
*
* @param mixed $encoded JSON encoded content.
* @return JsonContent Instance representing the provided content.
*/
public static function fromEncoded(mixed $encoded): JsonContent {
return new JsonContent(json_decode($encoded));
}
/**
* Creates an instance from an encoded file.
*
* @param string $path Path to the JSON encoded file.
* @return JsonContent Instance representing the provided path.
*/
public static function fromFile(string $path): JsonContent {
return self::fromEncoded(FileStream::openRead($path));
}
/**
* Creates an instance from the raw request body.
*
* @return JsonContent Instance representing the request body.
*/
public static function fromRequest(): JsonContent {
return self::fromFile('php://input');
}

View file

@ -1,21 +1,31 @@
<?php
// StreamContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http\Content;
use Stringable;
use Index\IO\Stream;
use Index\IO\FileStream;
class StreamContent implements Stringable, IHttpContent {
/**
* Represents Stream body content for a HTTP message.
*/
class StreamContent implements IHttpContent {
private Stream $stream;
/**
* @param Stream $stream Stream that represents this message body.
*/
public function __construct(Stream $stream) {
$this->stream = $stream;
}
/**
* Retrieves the underlying stream.
*
* @return Stream Underlying stream.
*/
public function getStream(): Stream {
return $this->stream;
}
@ -24,12 +34,23 @@ class StreamContent implements Stringable, IHttpContent {
return (string)$this->stream;
}
/**
* Creates an instance from a file.
*
* @param string $path Path to the file.
* @return StreamContent Instance representing the provided path.
*/
public static function fromFile(string $path): StreamContent {
return new StreamContent(
FileStream::openRead($path)
);
}
/**
* Creates an instance from the raw request body.
*
* @return StreamContent Instance representing the request body.
*/
public static function fromRequest(): StreamContent {
return self::fromFile('php://input');
}

View file

@ -1,19 +1,30 @@
<?php
// StringContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http\Content;
use Stringable;
class StringContent implements Stringable, IHttpContent {
/**
* Represents string body content for a HTTP message.
*/
class StringContent implements IHttpContent {
private string $string;
/**
* @param string $string String that represents this message body.
*/
public function __construct(string $string) {
$this->string = $string;
}
/**
* Retrieves the underlying string.
*
* @return string Underlying string.
*/
public function getString(): string {
return $this->string;
}
@ -22,14 +33,31 @@ class StringContent implements Stringable, IHttpContent {
return $this->string;
}
public static function fromObject(string $string): StringContent {
return new StringContent($string);
/**
* Creates an instance an existing object.
*
* @param Stringable|string $string Object to cast to a string.
* @return StringContent Instance representing the provided object.
*/
public static function fromObject(Stringable|string $string): StringContent {
return new StringContent((string)$string);
}
/**
* Creates an instance from a file.
*
* @param string $path Path to the file.
* @return StringContent Instance representing the provided path.
*/
public static function fromFile(string $path): StringContent {
return new StringContent(file_get_contents($path));
}
/**
* Creates an instance from the raw request body.
*
* @return StringContent Instance representing the request body.
*/
public static function fromRequest(): StringContent {
return self::fromFile('php://input');
}

View file

@ -1,7 +1,7 @@
<?php
// BencodeContentHandler.php
// Created: 2024-03-28
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http\ContentHandling;
@ -9,6 +9,9 @@ use Index\Bencode\IBencodeSerialisable;
use Index\Http\HttpResponseBuilder;
use Index\Http\Content\BencodedContent;
/**
* Represents a Bencode content handler for building HTTP response messages.
*/
class BencodeContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return $content instanceof IBencodeSerialisable;

View file

@ -1,13 +1,29 @@
<?php
// IContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ContentHandling;
use Index\Http\HttpResponseBuilder;
/**
* Represents a content handler for building HTTP response messages.
*/
interface IContentHandler {
/**
* Determines whether this handler is suitable for the body content.
*
* @param mixed $content Content to be judged.
* @return bool true if suitable, false if not.
*/
function match(mixed $content): bool;
/**
* Handles body content.
*
* @param HttpResponseBuilder $response Response to apply this body to.
* @param mixed $content Body to apply to the response message.
*/
function handle(HttpResponseBuilder $response, mixed $content): void;
}

View file

@ -1,7 +1,7 @@
<?php
// JsonContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ContentHandling;
@ -10,6 +10,9 @@ use JsonSerializable;
use Index\Http\HttpResponseBuilder;
use Index\Http\Content\JsonContent;
/**
* Represents a JSON content handler for building HTTP response messages.
*/
class JsonContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return is_array($content) || $content instanceof JsonSerializable || $content instanceof stdClass;

View file

@ -1,7 +1,7 @@
<?php
// StreamContentHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ContentHandling;
@ -9,6 +9,9 @@ use Index\Http\HttpResponseBuilder;
use Index\Http\Content\StreamContent;
use Index\IO\Stream;
/**
* Represents a Stream content handler for building HTTP response messages.
*/
class StreamContentHandler implements IContentHandler {
public function match(mixed $content): bool {
return $content instanceof Stream;

View file

@ -1,12 +1,15 @@
<?php
// HtmlErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ErrorHandling;
use Index\Http\{HttpResponseBuilder,HttpRequest};
/**
* Represents a basic HTML error message handler for building HTTP response messages.
*/
class HtmlErrorHandler implements IErrorHandler {
private const TEMPLATE = <<<HTML
<!doctype html>

View file

@ -1,13 +1,24 @@
<?php
// IErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ErrorHandling;
use Index\Http\HttpResponseBuilder;
use Index\Http\HttpRequest;
/**
* Represents an error message handler for building HTTP response messages.
*/
interface IErrorHandler {
/**
* Applies an error message template to the provided HTTP response builder.
*
* @param HttpResponseBuilder $response HTTP Response builder to apply this error to.
* @param HttpRequest $request HTTP Request this error is a response to.
* @param int $code HTTP status code to apply.
* @param string $message HTTP status message to apply.
*/
function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void;
}

View file

@ -1,12 +1,15 @@
<?php
// PlainErrorHandler.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\ErrorHandling;
use Index\Http\{HttpResponseBuilder,HttpRequest};
/**
* Represents a plain text error message handler for building HTTP response messages.
*/
class PlainErrorHandler implements IErrorHandler {
public function handle(HttpResponseBuilder $response, HttpRequest $request, int $code, string $message): void {
$response->setTypePlain();

View file

@ -1,29 +1,51 @@
<?php
// HttpHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http;
use Stringable;
/**
* Represents a generic HTTP header.
*/
class HttpHeader implements Stringable {
private string $name;
private array $lines;
/**
* @param string $name Name of the header.
* @param string ...$lines Lines of the header.
*/
public function __construct(string $name, string ...$lines) {
$this->name = $name;
$this->lines = $lines;
}
/**
* Retrieves the name of the header.
*
* @return string Header name.
*/
public function getName(): string {
return $this->name;
}
/**
* Retrieves lines of the header.
*
* @return string[] Header lines.
*/
public function getLines(): array {
return $this->lines;
}
/**
* Retrieves the first line of the header.
*
* @return string First header line.
*/
public function getFirstLine(): string {
return $this->lines[0];
}

View file

@ -1,15 +1,21 @@
<?php
// HttpHeaders.php
// Created: 2022-02-08
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http;
use RuntimeException;
/**
* Represents a collection of HTTP headers.
*/
class HttpHeaders {
private array $headers;
/**
* @param HttpHeader[] $headers HTTP header instances.
*/
public function __construct(array $headers) {
$real = [];
@ -20,14 +26,32 @@ class HttpHeaders {
$this->headers = $real;
}
/**
* Checks if a header is present.
*
* @param string $name Name of the header.
* @return bool true if the header is present.
*/
public function hasHeader(string $name): bool {
return isset($this->headers[strtolower($name)]);
}
/**
* Retrieves all headers.
*
* @return HttpHeader[] All headers.
*/
public function getHeaders(): array {
return array_values($this->headers);
}
/**
* Retrieves a header by name.
*
* @param string $name Name of the header.
* @throws RuntimeException If no header with $name exists.
* @return HttpHeader Instance of the requested header.
*/
public function getHeader(string $name): HttpHeader {
$name = strtolower($name);
if(!isset($this->headers[$name]))
@ -36,18 +60,36 @@ class HttpHeaders {
return $this->headers[$name];
}
/**
* Gets the contents of a header as a string.
*
* @param string $name Name of the header.
* @return string Contents of the header.
*/
public function getHeaderLine(string $name): string {
if(!$this->hasHeader($name))
return '';
return (string)$this->getHeader($name);
}
/**
* Gets lines of a header.
*
* @param string $name Name of the header.
* @return string[] Header lines.
*/
public function getHeaderLines(string $name): array {
if(!$this->hasHeader($name))
return [];
return $this->getHeader($name)->getLines();
}
/**
* Gets the first line of a header.
*
* @param string $name Name of the header.
* @return string First line of the header.
*/
public function getHeaderFirstLine(string $name): string {
if(!$this->hasHeader($name))
return '';

View file

@ -1,15 +1,25 @@
<?php
// HttpHeadersBuilder.php
// Created: 2022-02-08
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http;
use InvalidArgumentException;
/**
* Represents a HTTP message header builder.
*/
class HttpHeadersBuilder {
private array $headers = [];
/**
* Adds a header to the HTTP message.
* If a header with the same name is already present, it will be appended with a new line.
*
* @param string $name Name of the header to add.
* @param mixed $value Value to apply for this header.
*/
public function addHeader(string $name, mixed $value): void {
$nameLower = strtolower($name);
if(!isset($this->headers[$nameLower]))
@ -17,18 +27,41 @@ class HttpHeadersBuilder {
$this->headers[$nameLower][] = $value;
}
/**
* Sets a header to the HTTP message.
* If a header with the same name is already present, it will be overwritten.
*
* @param string $name Name of the header to set.
* @param mixed $value Value to apply for this header.
*/
public function setHeader(string $name, mixed $value): void {
$this->headers[strtolower($name)] = [$name, $value];
}
/**
* Removes a header from the HTTP message.
*
* @param string $name Name of the header to remove.
*/
public function removeHeader(string $name): void {
unset($this->headers[strtolower($name)]);
}
/**
* Checks if a header is already present.
*
* @param string $name Name of the header.
* @return bool true if it is present.
*/
public function hasHeader(string $name): bool {
return isset($this->headers[strtolower($name)]);
}
/**
* Create HttpHeaders instance from this builder.
*
* @return HttpHeaders Instance containing HTTP headers.
*/
public function toHeaders(): HttpHeaders {
$headers = [];

View file

@ -1,10 +1,11 @@
<?php
// HttpMessage.php
// Created: 2022-02-08
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http;
use RuntimeException;
use Index\IO\Stream;
use Index\Http\Content\IHttpContent;
use Index\Http\Content\BencodedContent;
@ -13,69 +14,153 @@ use Index\Http\Content\JsonContent;
use Index\Http\Content\StreamContent;
use Index\Http\Content\StringContent;
/**
* Represents a base HTTP message.
*/
abstract class HttpMessage {
private string $version;
private HttpHeaders $headers;
private ?IHttpContent $content;
/**
* @param string $version HTTP message version.
* @param HttpHeaders $headers HTTP message headers.
* @param ?IHttpContent $content Body contents.
*/
public function __construct(string $version, HttpHeaders $headers, ?IHttpContent $content) {
$this->version = $version;
$this->headers = $headers;
$this->content = $content;
}
/**
* Retrieves the HTTP version of this message.
*
* @return string HTTP version.
*/
public function getHttpVersion(): string {
return $this->version;
}
/**
* Retrieves the collection of headers for this HTTP message.
*
* @return HttpHeader[] HTTP headers.
*/
public function getHeaders(): array {
return $this->headers->getHeaders();
}
/**
* Checks if a header is present.
*
* @param string $name Name of the header.
* @return bool true if the header is present.
*/
public function hasHeader(string $name): bool {
return $this->headers->hasHeader($name);
}
/**
* Retrieves a header by name.
*
* @param string $name Name of the header.
* @throws RuntimeException If no header with $name exists.
* @return HttpHeader Instance of the requested header.
*/
public function getHeader(string $name): HttpHeader {
return $this->headers->getHeader($name);
}
/**
* Gets the contents of a header as a string.
*
* @param string $name Name of the header.
* @return string Contents of the header.
*/
public function getHeaderLine(string $name): string {
return $this->headers->getHeaderLine($name);
}
/**
* Gets lines of a header.
*
* @param string $name Name of the header.
* @return string[] Header lines.
*/
public function getHeaderLines(string $name): array {
return $this->headers->getHeaderLines($name);
}
/**
* Gets the first line of a header.
*
* @param string $name Name of the header.
* @return string First line of the header.
*/
public function getHeaderFirstLine(string $name): string {
return $this->headers->getHeaderFirstLine($name);
}
/**
* Checks whether this HTTP message has body contents.
*
* @return bool true if it has body contents.
*/
public function hasContent(): bool {
return $this->content !== null;
}
/**
* Retrieves message body contents, if present.
*
* @return ?IHttpContent Body contents, null if none present.
*/
public function getContent(): ?IHttpContent {
return $this->content;
}
/**
* Checks if the body content is JsonContent.
*
* @return bool true if it is JsonContent.
*/
public function isJsonContent(): bool {
return $this->content instanceof JsonContent;
}
/**
* Checks if the body content is FormContent.
*
* @return bool true if it is FormContent.
*/
public function isFormContent(): bool {
return $this->content instanceof FormContent;
}
/**
* Checks if the body content is StreamContent.
*
* @return bool true if it is StreamContent.
*/
public function isStreamContent(): bool {
return $this->content instanceof StreamContent;
}
/**
* Checks if the body content is StringContent.
*
* @return bool true if it is StringContent.
*/
public function isStringContent(): bool {
return $this->content instanceof StringContent;
}
/**
* Checks if the body content is BencodedContent.
*
* @return bool true if it is BencodedContent.
*/
public function isBencodedContent(): bool {
return $this->content instanceof BencodedContent;
}

View file

@ -1,7 +1,7 @@
<?php
// HttpMessageBuilder.php
// Created: 2022-02-08
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http;
@ -10,6 +10,9 @@ use Index\Http\Content\IHttpContent;
use Index\Http\Content\StreamContent;
use Index\Http\Content\StringContent;
/**
* Represents a base HTTP message builder.
*/
class HttpMessageBuilder {
private ?string $version = null;
private HttpHeadersBuilder $headers;
@ -19,47 +22,107 @@ class HttpMessageBuilder {
$this->headers = new HttpHeadersBuilder;
}
/**
* Returns HTTP version of the message.
*
* @return string HTTP version.
*/
protected function getHttpVersion(): string {
return $this->version ?? '1.1';
}
/**
* Sets HTTP version for the message.
*
* @param string $version HTTP version.
*/
public function setHttpVersion(string $version): void {
$this->version = $version;
}
/**
* Create HttpHeaders instance from this builder.
*
* @return HttpHeaders Instance containing HTTP headers.
*/
protected function getHeaders(): HttpHeaders {
return $this->headers->toHeaders();
}
/**
* Retrieves instance of the underlying HTTP header builder.
*
* @return HttpHeadersBuilder HTTP header builder.
*/
public function getHeadersBuilder(): HttpHeadersBuilder {
return $this->headers;
}
/**
* Adds a header to the HTTP message.
* If a header with the same name is already present, it will be appended with a new line.
*
* @param string $name Name of the header to add.
* @param mixed $value Value to apply for this header.
*/
public function addHeader(string $name, mixed $value): void {
$this->headers->addHeader($name, $value);
}
/**
* Sets a header to the HTTP message.
* If a header with the same name is already present, it will be overwritten.
*
* @param string $name Name of the header to set.
* @param mixed $value Value to apply for this header.
*/
public function setHeader(string $name, mixed $value): void {
$this->headers->setHeader($name, $value);
}
/**
* Removes a header from the HTTP message.
*
* @param string $name Name of the header to remove.
*/
public function removeHeader(string $name): void {
$this->headers->removeHeader($name);
}
/**
* Checks if a header is already present.
*
* @param string $name Name of the header.
* @return bool true if it is present.
*/
public function hasHeader(string $name): bool {
return $this->headers->hasHeader($name);
}
/**
* Retrieves HTTP message body content.
*
* @return ?IHttpContent Body content.
*/
protected function getContent(): ?IHttpContent {
return $this->content;
}
/** @phpstan-impure */
/**
* Checks whether this HTTP message has body contents.
*
* @return bool true if it has body contents.
* @phpstan-impure
*/
public function hasContent(): bool {
return $this->content !== null;
}
/**
* Sets HTTP message body contents.
*
* @param IHttpContent|Stream|string|null $content Body contents
*/
public function setContent(IHttpContent|Stream|string|null $content): void {
if($content instanceof Stream)
$content = new StreamContent($content);

View file

@ -1,7 +1,7 @@
<?php
// HttpRequest.php
// Created: 2022-02-08
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http;
@ -13,12 +13,24 @@ use Index\Http\Content\JsonContent;
use Index\Http\Content\StreamContent;
use Index\Http\Content\FormContent;
/**
* Represents a HTTP request message.
*/
class HttpRequest extends HttpMessage {
private string $method;
private string $path;
private array $params;
private array $cookies;
/**
* @param string $version HTTP message version.
* @param string $method HTTP request method.
* @param string $path HTTP request path.
* @param array<string, mixed> $params HTTP request query parameters.
* @param array<string, string> $cookies HTTP request cookies.
* @param HttpHeaders $headers HTTP message headers.
* @param ?IHttpContent $content Body contents.
*/
public function __construct(
string $version,
string $method,
@ -36,46 +48,105 @@ class HttpRequest extends HttpMessage {
parent::__construct($version, $headers, $content);
}
/**
* Retrieves the HTTP request method.
*
* @return string HTTP request method.
*/
public function getMethod(): string {
return $this->method;
}
/**
* Retrieves the HTTP request path.
*
* @return string HTTP request path.
*/
public function getPath(): string {
return $this->path;
}
/**
* Retrieves all HTTP request query fields as a query string.
*
* @param bool $spacesAsPlus true if spaces should be represented with a +, false if %20.
* @return string Query string representation of query fields.
*/
public function getParamString(bool $spacesAsPlus = false): string {
return http_build_query($this->params, '', '&', $spacesAsPlus ? PHP_QUERY_RFC1738 : PHP_QUERY_RFC3986);
}
/**
* Retrieves all HTTP request query fields.
*
* @return array<string, mixed> Query fields.
*/
public function getParams(): array {
return $this->params;
}
/**
* Retrieves an HTTP request query field, or null if it is not present.
*
* @param string $name Name of the request query field.
* @param int $filter A PHP filter extension filter constant.
* @param array|int $options Options for the PHP filter.
* @return mixed Value of the query field, null if not present.
*/
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
if(!isset($this->params[$name]))
return null;
return filter_var($this->params[$name] ?? null, $filter, $options);
}
/**
* Checks if a query field is present.
*
* @param string $name Name of the query field.
* @return bool true if the field is present, false if not.
*/
public function hasParam(string $name): bool {
return isset($this->params[$name]);
}
/**
* Retrieves all HTTP request cookies.
*
* @return array<string, string> All cookies.
*/
public function getCookies(): array {
return $this->cookies;
}
/**
* Retrieves an HTTP request cookie, or null if it is not present.
*
* @param string $name Name of the request cookie.
* @param int $filter A PHP filter extension filter constant.
* @param array|int $options Options for the PHP filter.
* @return mixed Value of the cookie, null if not present.
*/
public function getCookie(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
if(!isset($this->cookies[$name]))
return null;
return filter_var($this->cookies[$name] ?? null, $filter, $options);
}
/**
* Checks if a cookie is present.
*
* @param string $name Name of the cookie.
* @return bool true if the cookie is present, false if not.
*/
public function hasCookie(string $name): bool {
return isset($this->cookies[$name]);
}
/**
* Creates an HttpRequest instance from the current request.
*
* @return HttpRequest An instance representing the current request.
*/
public static function fromRequest(): HttpRequest {
$build = new HttpRequestBuilder;
$build->setHttpVersion($_SERVER['SERVER_PROTOCOL']);

View file

@ -1,64 +1,134 @@
<?php
// HttpRequestBuilder.php
// Created: 2022-02-08
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http;
/**
* Represents a HTTP request message builder.
*/
class HttpRequestBuilder extends HttpMessageBuilder {
private string $method = 'GET';
private string $path = '/';
private array $params = [];
private array $cookies = [];
/**
* Returns HTTP request method.
*
* @return string HTTP request method.
*/
protected function getMethod(): string {
return $this->method;
}
/**
* Sets HTTP request method.
*
* @param string $method HTTP request method.
*/
public function setMethod(string $method): void {
$this->method = $method;
}
/**
* Returns HTTP request path.
*
* @return string HTTP request path.
*/
protected function getPath(): string {
return $this->path;
}
/**
* Sets HTTP request path.
*
* @param string $path HTTP request path.
*/
public function setPath(string $path): void {
$this->path = $path;
}
/**
* Returns HTTP request query params.
*
* @return array<string, mixed> HTTP request query params.
*/
protected function getParams(): array {
return $this->params;
}
/**
* Sets HTTP request query params.
*
* @param array<string, mixed> $params HTTP request query params.
*/
public function setParams(array $params): void {
$this->params = $params;
}
/**
* Sets a HTTP request query param.
*
* @param string $name Name of the query field.
* @param mixed $value Value of the query field.
*/
public function setParam(string $name, mixed $value): void {
$this->params[$name] = $value;
}
/**
* Removes a HTTP request query param.
*
* @param string $name Name of the query field.
*/
public function removeParam(string $name): void {
unset($this->params[$name]);
}
/**
* Returns HTTP request cookies.
*
* @return array<string, string> HTTP request cookies.
*/
protected function getCookies(): array {
return $this->cookies;
}
/**
* Sets HTTP request cookies.
*
* @param array<string, string> $cookies HTTP request cookies.
*/
public function setCookies(array $cookies): void {
$this->cookies = $cookies;
}
/**
* Sets a HTTP request cookie.
*
* @param string $name Name of the cookie.
* @param mixed $value Value of the cookie.
*/
public function setCookie(string $name, mixed $value): void {
$this->cookies[$name] = $value;
}
/**
* Removes a HTTP request cookie.
*
* @param string $name Name of the cookie.
*/
public function removeCookie(string $name): void {
unset($this->cookies[$name]);
}
/**
* Creates a HttpRequest instance from this builder.
*
* @return HttpRequest An instance representing this builder.
*/
public function toRequest(): HttpRequest {
return new HttpRequest(
$this->getHttpVersion(),

View file

@ -1,16 +1,26 @@
<?php
// HttpResponse.php
// Created: 2022-02-08
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http;
use Index\Http\Content\IHttpContent;
/**
* Represents a HTTP response message.
*/
class HttpResponse extends HttpMessage {
private int $statusCode;
private string $statusText;
/**
* @param string $version HTTP message version.
* @param int $statusCode HTTP response status code.
* @param string $statusText HTTP response status text.
* @param HttpHeaders $headers HTTP message headers.
* @param ?IHttpContent $content Body contents.
*/
public function __construct(
string $version,
int $statusCode,
@ -24,10 +34,20 @@ class HttpResponse extends HttpMessage {
parent::__construct($version, $headers, $content);
}
/**
* Retrieves the HTTP response message status code.
*
* @return int HTTP status code.
*/
public function getStatusCode(): int {
return $this->statusCode;
}
/**
* Retrieves the HTTP response message status text.
*
* @return string HTTP status text.
*/
public function getStatusText(): string {
return $this->statusText;
}

View file

@ -1,7 +1,7 @@
<?php
// HttpResponseBuilder.php
// Created: 2022-02-08
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http;
@ -11,35 +11,78 @@ use Index\MediaType;
use Index\XDateTime;
use Index\Performance\Timings;
/**
* Represents a HTTP response message builder.
*/
class HttpResponseBuilder extends HttpMessageBuilder {
private int $statusCode = -1;
private ?string $statusText;
private array $vary = [];
/**
* Retrieves the HTTP status code for the target HTTP response message.
*
* @return int HTTP status code.
*/
public function getStatusCode(): int {
return $this->statusCode < 0 ? 200 : $this->statusCode;
}
/**
* Sets the HTTP status code for the target HTTP response message.
*
* @param int $statusCode HTTP status code.
*/
public function setStatusCode(int $statusCode): void {
$this->statusCode = $statusCode;
}
/**
* Checks whether this response message builder has a status code.
*
* @return bool true if it has a status code greater than and including 100.
*/
public function hasStatusCode(): bool {
return $this->statusCode >= 100;
}
/**
* Retrieves the status text for this response.
*
* @return string Status text.
*/
public function getStatusText(): string {
return $this->statusText ?? self::STATUS[$this->getStatusCode()] ?? 'Unknown Status';
}
/**
* Sets the status text for this response.
*
* @param string $statusText Status text.
*/
public function setStatusText(string $statusText): void {
$this->statusText = (string)$statusText;
}
/**
* Clears the status text for this message.
*/
public function clearStatusText(): void {
$this->statusText = null;
}
/**
* Adds a cookie to this response.
*
* @param string $name Name of the cookie.
* @param mixed $value Value of the cookie.
* @param DateTimeInterface|int|null $expires Point at which the cookie should expire.
* @param string $path Path to which to apply the cookie.
* @param string $domain Domain name to which to apply the cookie.
* @param bool $secure true to only make the client include this cookie in a secure context.
* @param bool $httpOnly true to make the client hide this cookie to Javascript code.
* @param bool $sameSiteStrict true to set the SameSite attribute to Strict, false to set it to Lax.
*/
public function addCookie(
string $name,
mixed $value,
@ -71,6 +114,16 @@ class HttpResponseBuilder extends HttpMessageBuilder {
$this->addHeader('Set-Cookie', $cookie);
}
/**
* Make the client remove a cookie.
*
* @param string $name Name of the cookie.
* @param string $path Path to which the cookie was applied to cookie.
* @param string $domain Domain name to which the cookie was applied to cookie.
* @param bool $secure true to only make the client include this cookie in a secure context.
* @param bool $httpOnly true to make the client hide this cookie to Javascript code.
* @param bool $sameSiteStrict true to set the SameSite attribute to Strict, false to set it to Lax.
*/
public function removeCookie(
string $name,
string $path = '',
@ -82,11 +135,22 @@ class HttpResponseBuilder extends HttpMessageBuilder {
$this->addCookie($name, '', -9001, $path, $domain, $secure, $httpOnly, $sameSiteStrict);
}
/**
* Specifies a redirect header on the response message.
*
* @param string $to Target to redirect to.
* @param bool $permanent true for status code 301, false for status code 302. Makes the client cache the redirect.
*/
public function redirect(string $to, bool $permanent = false): void {
$this->setStatusCode($permanent ? 301 : 302);
$this->setHeader('Location', $to);
}
/**
* Sets a Vary header.
*
* @param string|array $headers Header or headers that will vary.
*/
public function addVary(string|array $headers): void {
if(!is_array($headers))
$headers = [$headers];
@ -100,10 +164,21 @@ class HttpResponseBuilder extends HttpMessageBuilder {
$this->setHeader('Vary', implode(', ', $this->vary));
}
/**
* Sets an X-Powered-By header.
*
* @param string $poweredBy Thing that your website is powered by.
*/
public function setPoweredBy(string $poweredBy): void {
$this->setHeader('X-Powered-By', $poweredBy);
}
/**
* Sets an ETag header.
*
* @param string $eTag Unique identifier for the current state of the content.
* @param bool $weak Whether to use weak matching.
*/
public function setEntityTag(string $eTag, bool $weak = false): void {
$eTag = '"' . $eTag . '"';
@ -113,6 +188,11 @@ class HttpResponseBuilder extends HttpMessageBuilder {
$this->setHeader('ETag', $eTag);
}
/**
* Sets a Server-Timing header.
*
* @param Timings $timings Timings to supply to the devtools.
*/
public function setServerTiming(Timings $timings): void {
$laps = $timings->getLaps();
$timings = [];
@ -129,50 +209,109 @@ class HttpResponseBuilder extends HttpMessageBuilder {
$this->setHeader('Server-Timing', implode(', ', $timings));
}
/**
* Whether a Content-Type header is present.
*
* @return bool true if a Content-Type header is present.
*/
public function hasContentType(): bool {
return $this->hasHeader('Content-Type');
}
/**
* Sets a Content-Type header.
*
* @param MediaType|string $mediaType Media type to set as the content type of the response body.
*/
public function setContentType(MediaType|string $mediaType): void {
$this->setHeader('Content-Type', (string)$mediaType);
}
/**
* Sets the Content-Type to 'application/octet-stream' for raw content.
*/
public function setTypeStream(): void {
$this->setContentType('application/octet-stream');
}
/**
* Sets the Content-Type to 'text/plain' with a provided character set for plain text content.
*
* @param string $charset Character set.
*/
public function setTypePlain(string $charset = 'us-ascii'): void {
$this->setContentType('text/plain; charset=' . $charset);
}
/**
* Sets the Content-Type to 'text/html' with a provided character set for HTML content.
*
* @param string $charset Character set.
*/
public function setTypeHTML(string $charset = 'utf-8'): void {
$this->setContentType('text/html; charset=' . $charset);
}
/**
* Sets the Content-Type to 'application/json' with a provided character set for JSON content.
*
* @param string $charset Character set.
*/
public function setTypeJson(string $charset = 'utf-8'): void {
$this->setContentType('application/json; charset=' . $charset);
}
/**
* Sets the Content-Type to 'application/xml' with a provided character set for XML content.
*
* @param string $charset Character set.
*/
public function setTypeXML(string $charset = 'utf-8'): void {
$this->setContentType('application/xml; charset=' . $charset);
}
/**
* Sets the Content-Type to 'text/css' with a provided character set for CSS content.
*
* @param string $charset Character set.
*/
public function setTypeCSS(string $charset = 'utf-8'): void {
$this->setContentType('text/css; charset=' . $charset);
}
/**
* Sets the Content-Type to 'application/javascript' with a provided character set for Javascript content.
*
* @param string $charset Character set.
*/
public function setTypeJS(string $charset = 'utf-8'): void {
$this->setContentType('application/javascript; charset=' . $charset);
}
/**
* Specifies an Apache Web Server X-Sendfile header.
*
* @param string $absolutePath Absolute path to the content to serve.
*/
public function sendFile(string $absolutePath): void {
$this->setHeader('X-Sendfile', $absolutePath);
}
/**
* Specifies an NGINX X-Accel-Redirect header.
*
* @param string $uri Relative URI to the content to serve.
*/
public function accelRedirect(string $uri): void {
$this->setHeader('X-Accel-Redirect', $uri);
}
/**
* Specifies a Content-Disposition header to supply a filename for the content.
*
* @param string $fileName Name of the file.
* @param bool $attachment true if the browser should prompt the user to download the body.
*/
public function setFileName(string $fileName, bool $attachment = false): void {
$this->setHeader(
'Content-Disposition',
@ -184,6 +323,11 @@ class HttpResponseBuilder extends HttpMessageBuilder {
);
}
/**
* Specifies a Clear-Site-Data header.
*
* @param string ...$directives Directives to specify.
*/
public function clearSiteData(string ...$directives): void {
$this->setHeader(
'Clear-Site-Data',
@ -191,10 +335,20 @@ class HttpResponseBuilder extends HttpMessageBuilder {
);
}
/**
* Specifies a Cache-Control header.
*
* @param string ...$directives Directives to specify.
*/
public function setCacheControl(string ...$directives): void {
$this->setHeader('Cache-Control', implode(', ', $directives));
}
/**
* Creates an instance of HttpResponse from this builder.
*
* @return HttpResponse An instance representing this builder.
*/
public function toResponse(): HttpResponse {
return new HttpResponse(
$this->getHttpVersion(),

View file

@ -1,7 +1,7 @@
<?php
// HttpUploadedFile.php
// Created: 2022-02-10
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Http;
@ -12,6 +12,9 @@ use Index\IO\FileStream;
use InvalidArgumentException;
use RuntimeException;
/**
* Represents an uploaded file in a multipart/form-data request.
*/
class HttpUploadedFile implements ICloseable {
private int $errorCode;
private int $size;
@ -21,6 +24,13 @@ class HttpUploadedFile implements ICloseable {
private bool $hasMoved = false;
private ?Stream $stream = null;
/**
* @param int $errorCode PHP file upload error code.
* @param int $size Size, as provided by the client.
* @param string $localFileName Filename generated by PHP on the server.
* @param string $suggestedFileName Filename included by the client.
* @param MediaType|string $suggestedMediaType Mediatype included by the client.
*/
public function __construct(
int $errorCode,
int $size,
@ -38,34 +48,75 @@ class HttpUploadedFile implements ICloseable {
$this->suggestedMediaType = $suggestedMediaType;
}
/**
* Retrieves the PHP file upload error code.
*
* @return int PHP file upload error code.
*/
public function getErrorCode(): int {
return $this->errorCode;
}
/**
* Retrieves the size of the uploaded file.
*
* @return int Size of uploaded file.
*/
public function getSize(): int {
return $this->size;
}
/**
* Retrieves the local path to the uploaded file.
*
* @return ?string Path to file, or null.
*/
public function getLocalFileName(): ?string {
return $this->localFileName;
}
/**
* Retrieves media type of the uploaded file.
*
* @return ?MediaType Type of file, or null.
*/
public function getLocalMediaType(): ?MediaType {
return MediaType::fromPath($this->localFileName);
}
/**
* Retrieves the suggested name for the uploaded file.
*
* @return string Suggested name for the file.
*/
public function getSuggestedFileName(): string {
return $this->suggestedFileName;
}
/**
* Retrieves the suggested media type for the uploaded file.
*
* @return MediaType Suggested type for the file.
*/
public function getSuggestedMediaType(): MediaType {
return $this->suggestedMediaType;
}
/**
* Checks whether the file has been moved to its final destination.
*
* @return bool true if it has been moved.
*/
public function hasMoved(): bool {
return $this->hasMoved;
}
/**
* Gets a read-only stream to the uploaded file.
*
* @throws RuntimeException If the file cannot be opened.
* @return Stream Read-only stream of the upload file.
*/
public function getStream(): Stream {
if($this->stream === null) {
if($this->errorCode !== UPLOAD_ERR_OK)
@ -79,6 +130,15 @@ class HttpUploadedFile implements ICloseable {
return $this->stream;
}
/**
* Moves the uploaded file to its final destination.
*
* @param string $path Path to move the file to.
* @throws RuntimeException If the file has already been moved.
* @throws RuntimeException If an upload error occurred.
* @throws InvalidArgumentException If the provided $path is not valid.
* @throws RuntimeException If the file failed to move.
*/
public function moveTo(string $path): void {
if($this->hasMoved)
throw new RuntimeException('This uploaded file has already been moved.');
@ -110,6 +170,11 @@ class HttpUploadedFile implements ICloseable {
$this->close();
}
/**
* Creates a HttpUploadedFile instance from an entry in the $_FILES superglobal.
*
* @return HttpUploadedFile Uploaded file info.
*/
public static function createFromFILE(array $file): self {
return new HttpUploadedFile(
$file['error'] ?? UPLOAD_ERR_NO_FILE,
@ -120,6 +185,11 @@ class HttpUploadedFile implements ICloseable {
);
}
/**
* Creates a collection of HttpUploadedFile instances from the $_FILES superglobal.
*
* @return array<string, mixed> Uploaded files.
*/
public static function createFromFILES(array $files): array {
if(empty($files))
return [];

View file

@ -1,7 +1,7 @@
<?php
// HttpDelete.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpDelete extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('DELETE', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpGet.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpGet extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('GET', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpOptions.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpOptions extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('OPTIONS', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpPatch.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpPatch extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('PATCH', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpPost.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpPost extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('POST', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpPut.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -12,6 +12,9 @@ use Attribute;
*/
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class HttpPut extends HttpRoute {
/**
* @param string $path Path this route represents.
*/
public function __construct(string $path) {
parent::__construct('PUT', $path);
}

View file

@ -1,7 +1,7 @@
<?php
// HttpRouter.php
// Created: 2024-03-28
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\Http\Routing;
@ -26,6 +26,11 @@ class HttpRouter implements IRouter {
private IErrorHandler $errorHandler;
/**
* @param string $charSet Default character set to specify when none is present.
* @param IErrorHandler|string $errorHandler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
* @param bool $registerDefaultContentHandlers true to register default content handlers for JSON, Bencode, etc.
*/
public function __construct(
string $charSet = '',
IErrorHandler|string $errorHandler = 'html',
@ -38,16 +43,31 @@ class HttpRouter implements IRouter {
$this->registerDefaultContentHandlers();
}
/**
* Retrieves the normalised name of the preferred character set.
*
* @return string Normalised character set name.
*/
public function getCharSet(): string {
if($this->defaultCharSet === '')
return strtolower(mb_preferred_mime_name(mb_internal_encoding()));
return $this->defaultCharSet;
}
/**
* Retrieves the error handler instance.
*
* @return IErrorHandler The error handler.
*/
public function getErrorHandler(): IErrorHandler {
return $this->errorHandler;
}
/**
* Changes the active error handler.
*
* @param IErrorHandler|string $handler Error handling to use for error responses with an empty body. 'html' for the default HTML implementation, 'plain' for the plaintext implementation.
*/
public function setErrorHandler(IErrorHandler|string $handler): void {
if($handler instanceof IErrorHandler)
$this->errorHandler = $handler;
@ -57,25 +77,45 @@ class HttpRouter implements IRouter {
$this->setPlainErrorHandler();
}
/**
* Set the error handler to the basic HTML one.
*/
public function setHTMLErrorHandler(): void {
$this->errorHandler = new HtmlErrorHandler;
}
/**
* Set the error handler to the plain text one.
*/
public function setPlainErrorHandler(): void {
$this->errorHandler = new PlainErrorHandler;
}
/**
* Register a message body content handler.
*
* @param IContentHandler $contentHandler Content handler to register.
*/
public function registerContentHandler(IContentHandler $contentHandler): void {
if(!in_array($contentHandler, $this->contentHandlers))
$this->contentHandlers[] = $contentHandler;
}
/**
* Register the default content handlers.
*/
public function registerDefaultContentHandlers(): void {
$this->registerContentHandler(new StreamContentHandler);
$this->registerContentHandler(new JsonContentHandler);
$this->registerContentHandler(new BencodeContentHandler);
}
/**
* Retrieve a scoped router to a given path prefix.
*
* @param string $prefix Prefix to apply to paths within the returned router.
* @return IRouter Scopes router proxy.
*/
public function scopeTo(string $prefix): IRouter {
return new ScopedRouter($this, $prefix);
}
@ -92,6 +132,12 @@ class HttpRouter implements IRouter {
return sprintf('#^%s%s#su', $path, $prefixMatch ? '' : '$');
}
/**
* Registers a middleware handler.
*
* @param string $path Path prefix or regex to apply this middleware on.
* @param callable $handler Middleware handler.
*/
public function use(string $path, callable $handler): void {
$this->middlewares[] = $mwInfo = new stdClass;
$mwInfo->handler = $handler;
@ -105,6 +151,15 @@ class HttpRouter implements IRouter {
$mwInfo->prefix = $path;
}
/**
* Registers a route handler for a given method and path.
*
* @param string $method Method to use this handler for.
* @param string $path Path or regex to use this handler with.
* @param callable $handler Handler to use for this method/path combination.
* @throws InvalidArgumentException If $method is empty.
* @throws InvalidArgumentException If $method starts or ends with spaces.
*/
public function add(string $method, string $path, callable $handler): void {
if($method === '')
throw new InvalidArgumentException('$method may not be empty');
@ -130,6 +185,13 @@ class HttpRouter implements IRouter {
}
}
/**
* Resolves middlewares and a route handler for a given method and path.
*
* @param string $method Method to resolve for.
* @param string $path Path to resolve for.
* @return ResolvedRouteInfo Resolved route information.
*/
public function resolve(string $method, string $path): ResolvedRouteInfo {
if(str_ends_with($path, '/'))
$path = substr($path, 0, -1);
@ -178,6 +240,12 @@ class HttpRouter implements IRouter {
return new ResolvedRouteInfo($middlewares, array_keys($methods), $handler, $args);
}
/**
* Dispatches a route based on a given HTTP request message with additional prefix arguments and output to stdout.
*
* @param ?HttpRequest $request HTTP request message to handle, null to use the current request.
* @param array $args Additional arguments to prepend to the argument list sent to the middleware and route handlers.
*/
public function dispatch(?HttpRequest $request = null, array $args = []): void {
$request ??= HttpRequest::fromRequest();
$response = new HttpResponseBuilder;
@ -232,11 +300,24 @@ class HttpRouter implements IRouter {
self::output($response->toResponse(), $request->getMethod() !== 'HEAD');
}
/**
* Writes an error page to a given HTTP response builder.
*
* @param HttpResponseBuilder $response HTTP response builder to apply the error page to.
* @param HttpRequest $request HTTP request that triggered this error.
* @param int $statusCode HTTP status code for this error page.
*/
public function writeErrorPage(HttpResponseBuilder $response, HttpRequest $request, int $statusCode): void {
$response->setStatusCode($statusCode);
$this->errorHandler->handle($response, $request, $response->getStatusCode(), $response->getStatusText());
}
/**
* Outputs a HTTP response message to stdout.
*
* @param HttpResponse $response HTTP response message to output.
* @param bool $includeBody true to include the response message body, false to omit it for HEAD requests.
*/
public static function output(HttpResponse $response, bool $includeBody): void {
header(sprintf(
'HTTP/%s %03d %s',

View file

@ -1,11 +1,20 @@
<?php
// ResolvedRouteInfo.php
// Created: 2024-03-28
// Updated: 2024-03-28
// Updated: 2024-08-01
namespace Index\Http\Routing;
/**
* Represents a resolved route.
*/
class ResolvedRouteInfo {
/**
* @param array $middlewares Middlewares that should be run prior to the route handler.
* @param array $supportedMethods HTTP methods that this route accepts.
* @param mixed $handler Route handler.
* @param array $args Argument list to pass to the middleware and route handlers.
*/
public function __construct(
private array $middlewares,
private array $supportedMethods,
@ -13,6 +22,12 @@ class ResolvedRouteInfo {
private array $args,
) {}
/**
* Run middleware handlers.
*
* @param array $args Additional arguments to pass to the middleware handlers.
* @return mixed Return value from the first middleware to return anything non-null, otherwise null.
*/
public function runMiddleware(array $args): mixed {
foreach($this->middlewares as $middleware) {
$result = $middleware[0](...array_merge($args, $middleware[1]));
@ -23,18 +38,39 @@ class ResolvedRouteInfo {
return null;
}
/**
* Whether this route has a handler.
*
* @return bool true if it does.
*/
public function hasHandler(): bool {
return $this->handler !== null;
}
/**
* Whether this route supports other HTTP request methods.
*
* @return bool true if it does.
*/
public function hasOtherMethods(): bool {
return !empty($this->supportedMethods);
}
/**
* Gets the list of supported HTTP request methods for this route.
*
* @return string[] Supported HTTP request methods.
*/
public function getSupportedMethods(): array {
return $this->supportedMethods;
}
/**
* Dispatches this route.
*
* @param array $args Additional arguments to pass to the route handler.
* @return mixed Return value of the route handler.
*/
public function dispatch(array $args): mixed {
return ($this->handler)(...array_merge($args, $this->args));
}

View file

@ -1,31 +1,121 @@
<?php
// FileStream.php
// Created: 2021-04-30
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\IO;
use ErrorException;
/**
* Represents a Stream representing a file.
*/
class FileStream extends GenericStream {
public const OPEN_READ = 'rb'; // O R E // Open file for reading, throw if not exists
public const OPEN_READ_WRITE = 'r+b'; // O RW E // Open file for reading and writing, throw if not exist
public const NEW_WRITE = 'wb'; // C W T // Create file for writing, truncate if exist
public const NEW_READ_WRITE = 'w+b'; // C RW T // Create file for reading and writing, truncate if exist
public const APPEND_WRITE = 'ab'; // OC A // Open file for appending, create if not exist
public const APPEND_READ_WRITE = 'a+b'; // OC R A // Open file for reading and appending, create if not exist
public const CREATE_WRITE = 'xb'; // C W T // Create file for writing, throw if exist
public const CREATE_READ_WRITE = 'x+b'; // C RW T // Create file for reading and writing, throw if exist
public const OPEN_OR_CREATE_WRITE = 'cb'; // OC W // Opens or creates a file for writing
public const OPEN_OR_CREATE_READ_WRITE = 'c+b'; // OC RW // Opens or creates a file for reading and writing
/**
* Open file for reading, throw if not exists.
*
* @var string
*/
public const OPEN_READ = 'rb';
public const LOCK_NONE = 0;
public const LOCK_READ = LOCK_SH;
public const LOCK_WRITE = LOCK_EX;
/**
* Open file for reading and writing, throw if not exist.
*
* @var string
*/
public const OPEN_READ_WRITE = 'r+b';
/**
* Create file for writing, truncate if exist.
*
* @var string
*/
public const NEW_WRITE = 'wb';
/**
* Create file for reading and writing, truncate if exist.
*
* @var string
*/
public const NEW_READ_WRITE = 'w+b';
/**
* Open file for appending, create if not exist.
*
* @var string
*/
public const APPEND_WRITE = 'ab';
/**
* Open file for reading and appending, create if not exist.
*
* @var string
*/
public const APPEND_READ_WRITE = 'a+b';
/**
* Create file for writing, throw if exist.
*
* @var string
*/
public const CREATE_WRITE = 'xb';
/**
* Create file for reading and writing, throw if exist.
*
* @var string
*/
public const CREATE_READ_WRITE = 'x+b';
/**
* Opens or creates a file for writing.
*
* @var string
*/
public const OPEN_OR_CREATE_WRITE = 'cb';
/**
* Opens or creates a file for reading and writing.
*
* @var string
*/
public const OPEN_OR_CREATE_READ_WRITE = 'c+b';
/**
* Don't lock file.
*
* @var int
*/
public const LOCK_NONE = 0;
/**
* Lock for reading.
*
* @var int
*/
public const LOCK_READ = LOCK_SH;
/**
* Lock for writing.
*
* @var int
*/
public const LOCK_WRITE = LOCK_EX;
/**
* Don't block during lock attempt.
*
* @var int
*/
public const LOCK_NON_BLOCKING = LOCK_NB;
private int $lock;
/**
* @param string $path Path to the file.
* @param string $mode Stream mode to use to open the file.
* @param int $lock Locking method to use.
*/
public function __construct(string $path, string $mode, int $lock) {
$this->lock = $lock;
@ -52,33 +142,112 @@ class FileStream extends GenericStream {
parent::close();
}
/**
* Open file for reading, throw if not exists.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function openRead(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::OPEN_READ, $lock);
}
/**
* Open file for reading and writing, throw if not exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function openReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::OPEN_READ_WRITE, $lock);
}
/**
* Create file for writing, truncate if exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function newWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::NEW_WRITE, $lock);
}
/**
* Create file for reading and writing, truncate if exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function newReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::NEW_READ_WRITE, $lock);
}
/**
* Open file for appending, create if not exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function appendWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::APPEND_WRITE, $lock);
}
/**
* Open file for reading and appending, create if not exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function appendReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::APPEND_READ_WRITE, $lock);
}
/**
* Create file for writing, throw if exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function createWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::CREATE_WRITE, $lock);
}
/**
* Create file for reading and writing, throw if exist.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function createReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::CREATE_READ_WRITE, $lock);
}
/**
* Opens or creates a file for writing.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function openOrCreateWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::OPEN_OR_CREATE_WRITE, $lock);
}
/**
* Opens or creates a file for reading and writing.
*
* @param string $path Path to the file.
* @param int $lock Locking method to use.
* @return FileStream Stream to file.
*/
public static function openOrCreateReadWrite(string $path, int $lock = self::LOCK_NONE): FileStream {
return new FileStream($path, self::OPEN_OR_CREATE_READ_WRITE, $lock);
}

View file

@ -1,17 +1,31 @@
<?php
// GenericStream.php
// Created: 2021-04-30
// Updated: 2023-07-17
// Updated: 2024-08-01
namespace Index\IO;
use InvalidArgumentException;
/**
* Represents any stream that can be handled using the C-like f* functions.
*/
class GenericStream extends Stream {
/**
* Readable file modes.
*
* @var string[]
*/
public const READABLE = [
'r', 'rb', 'r+', 'r+b', 'w+', 'w+b',
'a+', 'a+b', 'x+', 'x+b', 'c+', 'c+b',
];
/**
* Writeable file modes.
*
* @var string[]
*/
public const WRITEABLE = [
'r+', 'r+b', 'w', 'wb', 'w+', 'w+b',
'a', 'ab', 'a+', 'a+b', 'x', 'xb',
@ -24,6 +38,9 @@ class GenericStream extends Stream {
private bool $canWrite;
private bool $canSeek;
/**
* @param resource $stream Resource that this stream represents.
*/
public function __construct($stream) {
if(!is_resource($stream) || get_resource_type($stream) !== 'stream')
throw new InvalidArgumentException('$stream is not a valid stream resource.');
@ -35,6 +52,11 @@ class GenericStream extends Stream {
$this->canSeek = $metaData['seekable'];
}
/**
* Returns the underlying resource handle.
*
* @return resource Underlying resource handle.
*/
public function getResource() {
return $this->stream;
}
@ -98,8 +120,8 @@ class GenericStream extends Stream {
return $buffer;
}
public function seek(int $offset, int $origin = Stream::START): int {
return fseek($this->stream, $offset, $origin);
public function seek(int $offset, int $origin = Stream::START): bool {
return fseek($this->stream, $offset, $origin) === 0;
}
public function write(string $buffer, int $length = -1): void {
@ -123,6 +145,7 @@ class GenericStream extends Stream {
} catch(\Error $ex) {}
}
#[\Override]
public function copyTo(Stream $other): void {
if($other instanceof GenericStream) {
stream_copy_to_stream($this->stream, $other->stream);

View file

@ -1,10 +1,13 @@
<?php
// IOException.php
// Created: 2021-04-30
// Updated: 2021-04-30
// Updated: 2024-08-01
namespace Index\IO;
use RuntimeException;
/**
* Exception type for the Index\IO namespace.
*/
class IOException extends RuntimeException {}

View file

@ -1,15 +1,24 @@
<?php
// MemoryStream.php
// Created: 2021-05-02
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\IO;
/**
* Represents an in-memory stream.
*/
class MemoryStream extends GenericStream {
public function __construct() {
parent::__construct(fopen('php://memory', 'r+b'));
}
/**
* Creates a memory stream from a string/binary data.
*
* @param string $string String to create the stream from.
* @return MemoryStream Stream representing the string.
*/
public static function fromString(string $string): MemoryStream {
$stream = new MemoryStream;
$stream->write($string);

View file

@ -1,7 +1,7 @@
<?php
// NetworkStream.php
// Created: 2021-04-30
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\IO;
@ -9,7 +9,16 @@ use ErrorException;
use Index\Net\IPAddress;
use Index\Net\EndPoint;
/**
* Represents a network socket stream.
*/
class NetworkStream extends GenericStream {
/**
* @param string $hostname Hostname to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
* @throws IOException If the socket failed to open.
*/
public function __construct(string $hostname, int $port, float|null $timeout) {
try {
$stream = fsockopen($hostname, $port, $errcode, $errmsg, $timeout);
@ -23,41 +32,118 @@ class NetworkStream extends GenericStream {
parent::__construct($stream);
}
/**
* Sets whether the network stream should have blocking reads and writes.
*
* @param bool $blocking true to block, false to async.
*/
public function setBlocking(bool $blocking): void {
stream_set_blocking($this->stream, $blocking);
}
/**
* Opens a network socket stream to an endpoint represented by an Index EndPoint instance.
*
* @param EndPoint $endPoint Host to connect to.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openEndPoint(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
return new NetworkStream((string)$endPoint, -1, $timeout);
}
/**
* Opens an SSL network socket stream to an endpoint represented by an Index EndPoint instance.
*
* @param EndPoint $endPoint Host to connect to.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openEndPointSSL(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
return new NetworkStream('ssl://' . ((string)$endPoint), -1, $timeout);
}
/**
* Opens a TLS network socket stream to an endpoint represented by an Index EndPoint instance.
*
* @param EndPoint $endPoint Host to connect to.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openEndPointTLS(EndPoint $endPoint, float|null $timeout = null): NetworkStream {
return new NetworkStream('tls://' . ((string)$endPoint), -1, $timeout);
}
/**
* Opens a network socket stream to an endpoint represented by an hostname and port.
*
* @param string $hostname Hostname to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openHost(string $hostname, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream($hostname, $port, $timeout);
}
/**
* Opens an SSL network socket stream to an endpoint represented by an hostname and port.
*
* @param string $hostname Hostname to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openHostSSL(string $hostname, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream('ssl://' . ((string)$hostname), $port, $timeout);
}
/**
* Opens a TLS network socket stream to an endpoint represented by an hostname and port.
*
* @param string $hostname Hostname to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openHostTLS(string $hostname, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream('tls://' . ((string)$hostname), $port, $timeout);
}
/**
* Opens a network socket stream to an endpoint represented by an Index IPAddress instance and port.
*
* @param IPAddress $address Address to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openAddress(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream($address->getAddress(), $port, $timeout);
}
/**
* Opens an SSL network socket stream to an endpoint represented by an Index IPAddress instance and port.
*
* @param IPAddress $address Address to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openAddressSSL(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream('ssl://' . $address->getAddress(), $port, $timeout);
}
/**
* Opens a TLS network socket stream to an endpoint represented by an Index IPAddress instance and port.
*
* @param IPAddress $address Address to connect to.
* @param int $port Port to connect at.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openAddressTLS(IPAddress $address, int $port, float|null $timeout = null): NetworkStream {
return new NetworkStream('tls://' . $address->getAddress(), $port, $timeout);
}
/**
* Opens a network socket stream to an endpoint represented by a UNIX socket path.
*
* @param string $path Path to connect to.
* @param float|null $timeout Amount of seconds until timeout, null for php.ini default.
*/
public static function openUnix(string $path, float|null $timeout = null): NetworkStream {
return new NetworkStream('unix://' . ((string)$path), -1, $timeout);
return new NetworkStream('unix://' . $path, -1, $timeout);
}
}

View file

@ -1,15 +1,23 @@
<?php
// ProcessStream.php
// Created: 2023-01-25
// Updated: 2023-01-25
// Updated: 2024-08-01
namespace Index\IO;
/**
* Represents a stream to a running sub-process.
*/
class ProcessStream extends Stream {
private $handle;
private bool $canRead;
private bool $canWrite;
/**
* @param string $command Command to run for this stream.
* @param string $mode File mode to use.
* @throws IOException If we were unable to spawn the process.
*/
public function __construct(string $command, string $mode) {
$this->handle = popen($command, $mode);
$this->canRead = strpos($mode, 'r') !== false;
@ -71,7 +79,7 @@ class ProcessStream extends Stream {
return $buffer;
}
public function seek(int $offset, int $origin = self::START): int {
public function seek(int $offset, int $origin = self::START): bool {
throw new IOException('Cannot seek ProcessStream.');
}

View file

@ -1,37 +1,113 @@
<?php
// Stream.php
// Created: 2021-04-30
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index\IO;
use Stringable;
use Index\ICloseable;
/**
* Represents a generic data stream.
*/
abstract class Stream implements Stringable, ICloseable {
/**
* Place the cursor relative to the start of the file.
*
* @var int
*/
public const START = SEEK_SET;
/**
* Place the cursor relative to the current position of the cursor.
*
* @var int
*/
public const CURRENT = SEEK_CUR;
/**
* Place the cursor relative to the end of the file.
*
* @var int
*/
public const END = SEEK_END;
/**
* Retrieves the current cursor position.
*
* @return int Cursor position.
*/
abstract public function getPosition(): int;
/**
* Retrieves the current length of the stream.
*
* @return int Stream length.
*/
abstract public function getLength(): int;
/**
* Sets the length of the stream.
*
* @param int $length Desired stream length.
*/
abstract public function setLength(int $length): void;
/**
* Whether this stream is readable.
*
* @return bool true if the stream is readable.
*/
abstract public function canRead(): bool;
/**
* Whether this stream is writeable.
*
* @return bool true if the stream is writeable.
*/
abstract public function canWrite(): bool;
/**
* Whether this stream is seekable.
*
* @return bool true if the stream is seekable.
*/
abstract public function canSeek(): bool;
/**
* Whether this stream has timed out.
*
* @return bool true if the stream has timed out.
*/
abstract public function hasTimedOut(): bool;
/**
* Whether this stream has blocking operations.
*
* @return bool true if the stream has blocking operations.
*/
abstract public function isBlocking(): bool;
/**
* Whether the cursor is at the end of the file.
*
* @return bool true if the cursor is at the end of the file.
*/
abstract public function isEnded(): bool;
/**
* Read a single character from the stream.
*
* @return ?string One character or null if we're at the end of the stream.
*/
abstract public function readChar(): ?string;
/**
* Read a single byte from the stream.
*
* @return int One byte or -1 if we're at the end of the stream.
*/
public function readByte(): int {
$char = $this->readChar();
if($char === null)
@ -39,25 +115,72 @@ abstract class Stream implements Stringable, ICloseable {
return ord($char);
}
/**
* Read a line of text from the stream.
*
* @return ?string One line of text or null if we're at the end of the stream.
*/
abstract public function readLine(): ?string;
/**
* Read an amount of data from the stream.
*
* @param int $length Maximum amount of data to read.
* @return ?string At most a $length sized string or null if we're at the end of the stream.
*/
abstract public function read(int $length): ?string;
abstract public function seek(int $offset, int $origin = self::START): int;
/**
* Move the cursor to a different place in the stream.
*
* @param int $offset Offset to apply to the cursor position.
* @param int $origin Point from which to apply the offset.
* @throws IOException If the stream is not seekable.
* @return bool true if the cursor offset was applied successfully.
*/
abstract public function seek(int $offset, int $origin = self::START): bool;
/**
* Write an amount of data to the stream.
*
* @param string $buffer Buffer to write from.
* @param int $length Amount of data to write from the buffer, or -1 to write the entire buffer.
*/
abstract public function write(string $buffer, int $length = -1): void;
/**
* Writes a line of text to the stream.
*
* @param string $line Line of text to write.
*/
public function writeLine(string $line): void {
$this->write($line);
$this->write(PHP_EOL);
}
/**
* Writes a single character to the stream.
*
* @param string $char Character to write to the stream.
*/
abstract public function writeChar(string $char): void;
/**
* Writes a single byte to the stream.
*
* @param int $byte Byte to write to the stream.
*/
public function writeByte(int $byte): void {
$this->writeChar(chr($byte & 0xFF));
}
/**
* Copy the entire contents of this stream to another.
*
* Will seek to the beginning of the stream if the stream is seekable.
*
* @param Stream $other Target stream.
*/
public function copyTo(Stream $other): void {
if($this->canSeek())
$this->seek(0);
@ -66,6 +189,9 @@ abstract class Stream implements Stringable, ICloseable {
$other->write($this->read(8192));
}
/**
* Force write of all buffered output to the stream.
*/
abstract public function flush(): void;
abstract public function close(): void;

View file

@ -1,13 +1,25 @@
<?php
// TempFileStream.php
// Created: 2021-05-02
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\IO;
/**
* Represents a temporary file stream. Will remain in memory if the size is below a given threshold.
*/
class TempFileStream extends GenericStream {
/**
* Default maximum size the stream can be to remain in memory and not be written to disk.
*
* @var int
*/
public const MAX_MEMORY = 2 * 1024 * 1024;
/**
* @param ?string $body Contents of the stream.
* @param int $maxMemory Maximum filesize until the file is moved out of memory and to disk.
*/
public function __construct(?string $body = null, int $maxMemory = self::MAX_MEMORY) {
parent::__construct(fopen("php://temp/maxmemory:{$maxMemory}", 'r+b'));
@ -17,6 +29,12 @@ class TempFileStream extends GenericStream {
}
}
/**
* Creates a TempFileStream from a string.
*
* @param string $string Data to write to the temporary file.
* @return TempFileStream Stream representing the given text.
*/
public static function fromString(string $string): TempFileStream {
return new TempFileStream($string);
}

View file

@ -1,13 +1,16 @@
<?php
// MediaType.php
// Created: 2022-02-10
// Updated: 2023-07-22
// Updated: 2024-08-01
namespace Index;
use InvalidArgumentException;
use Stringable;
/**
* Implements a structure for representing and comparing media/mime types.
*/
class MediaType implements Stringable, IComparable, IEquatable {
private string $category = '';
private string $kind = '';

View file

@ -1,17 +1,24 @@
<?php
// DnsEndPoint.php
// Created: 2021-05-02
// Updated: 2023-01-01
// Updated: 2024-08-01
namespace Index\Net;
use InvalidArgumentException;
use Stringable;
class DnsEndPoint extends EndPoint implements Stringable {
/**
* A DNS end point.
*/
class DnsEndPoint extends EndPoint {
private string $host;
private int $port;
/**
* @param string $host DNS host name.
* @param int $port Network port, 0 to leave default.
* @throws InvalidArgumentException If $port is less than 0 or greater than 65535.
*/
public function __construct(string $host, int $port) {
if($port < 0 || $port > 0xFFFF)
throw new InvalidArgumentException('$port is not a valid port number.');
@ -19,14 +26,29 @@ class DnsEndPoint extends EndPoint implements Stringable {
$this->port = $port;
}
/**
* Retrieves host name.
*
* @return string Host name.
*/
public function getHost(): string {
return $this->host;
}
/**
* Whether a port is specified.
*
* @return bool true if the port number is greater than 0.
*/
public function hasPort(): bool {
return $this->port > 0;
}
/**
* Retrieves port number.
*
* @return int Network port number.
*/
public function getPort(): int {
return $this->port;
}
@ -34,6 +56,7 @@ class DnsEndPoint extends EndPoint implements Stringable {
public function __serialize(): array {
return [$this->host, $this->port];
}
public function __unserialize(array $serialized): void {
$this->host = $serialized[0];
$this->port = $serialized[1];
@ -45,11 +68,18 @@ class DnsEndPoint extends EndPoint implements Stringable {
&& $this->host === $other->host;
}
public static function parse(string $string): EndPoint {
/**
* Attempts to parse a string into a DNS end point.
*
* @param string $string String to parse.
* @throws InvalidArgumentException If $string does not contain a valid DNS end point string.
* @return DnsEndPoint Representation of the given string.
*/
public static function parse(string $string): DnsEndPoint {
$parts = explode(':', $string);
if(count($parts) < 2)
throw new InvalidArgumentException('$string is not a valid host and port combo.');
return new DnsEndPoint($parts[0], (int)$parts[1]);
return new DnsEndPoint($parts[0], (int)($parts[1] ?? '0'));
}
public function __toString(): string {

View file

@ -1,7 +1,7 @@
<?php
// EndPoint.php
// Created: 2021-04-30
// Updated: 2022-02-28
// Updated: 2024-08-01
namespace Index\Net;
@ -10,6 +10,9 @@ use JsonSerializable;
use Stringable;
use Index\IEquatable;
/**
* Represents a generic network end point.
*/
abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
abstract public function equals(mixed $other): bool;
abstract public function __toString(): string;
@ -18,29 +21,36 @@ abstract class EndPoint implements JsonSerializable, Stringable, IEquatable {
return (string)$this;
}
public static function parse(string $endPoint): EndPoint {
if($endPoint === '')
throw new InvalidArgumentException('$endPoint is not a valid endpoint string.');
/**
* Attempts to parse a string into a known end point object.
*
* @param string $string String to parse.
* @throws InvalidArgumentException If $string does not contain a valid end point string.
* @return EndPoint Representation of the given string.
*/
public static function parse(string $string): EndPoint {
if($string === '')
throw new InvalidArgumentException('$string is not a valid endpoint string.');
$firstChar = $endPoint[0];
$firstChar = $string[0];
if($firstChar === '/' || str_starts_with($endPoint, 'unix:'))
$endPoint = UnixEndPoint::parse($endPoint);
if($firstChar === '/' || str_starts_with($string, 'unix:'))
$endPoint = UnixEndPoint::parse($string);
elseif($firstChar === '[') { // IPv6
if(str_contains($endPoint, ']:'))
$endPoint = IPEndPoint::parse($endPoint);
if(str_contains($string, ']:'))
$endPoint = IPEndPoint::parse($string);
else
$endPoint = new IPEndPoint(IPAddress::parse(trim($endPoint, '[]')), 0);
$endPoint = new IPEndPoint(IPAddress::parse(trim($string, '[]')), 0);
} elseif(is_numeric($firstChar)) { // IPv4
if(str_contains($endPoint, ':'))
$endPoint = IPEndPoint::parse($endPoint);
if(str_contains($string, ':'))
$endPoint = IPEndPoint::parse($string);
else
$endPoint = new IPEndPoint(IPAddress::parse($endPoint), 0);
$endPoint = new IPEndPoint(IPAddress::parse($string), 0);
} else { // DNS
if(str_contains($endPoint, ':'))
$endPoint = DnsEndPoint::parse($endPoint);
if(str_contains($string, ':'))
$endPoint = DnsEndPoint::parse($string);
else
$endPoint = new DnsEndPoint($endPoint, 0);
$endPoint = new DnsEndPoint($string, 0);
}
return $endPoint;

View file

@ -1,7 +1,7 @@
<?php
// IPAddress.php
// Created: 2021-04-26
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Net;
@ -10,33 +10,89 @@ use JsonSerializable;
use Stringable;
use Index\IEquatable;
/**
* Represents an IP address.
*/
final class IPAddress implements JsonSerializable, Stringable, IEquatable {
/**
* Unknown IP version.
*
* @var int
*/
public const UNKNOWN = 0;
/**
* IPv4
*
* @var int
*/
public const V4 = 4;
/**
* IPv6
*
* @var int
*/
public const V6 = 6;
private string $raw;
private int $width;
/**
* @param string $raw Raw IP address data.
*/
public function __construct(string $raw) {
$this->raw = $raw;
$this->width = strlen($raw);
}
/**
* Get raw address bytes.
*
* @return string Raw address bytes.
*/
public function getRaw(): string {
return $this->raw;
}
/**
* Gets the width of the raw address byte data.
*
* 4 bytes for IPv4
* 16 bytes for IPv6
*
* @return int Raw address byte width.
*/
public function getWidth(): int {
return $this->width;
}
/**
* Gets safe string representation of IP address.
*
* IPv6 addresses will be wrapped in square brackets.
* Use getCleanAddress if this is undesirable.
*
* @return string Safe string IP address.
*/
public function getAddress(): string {
return sprintf($this->isV6() ? '[%s]' : '%s', inet_ntop($this->raw));
}
/**
* Gets string representation of IP address.
*
* @return string String IP address.
*/
public function getCleanAddress(): string {
return inet_ntop($this->raw);
}
/**
* Gets IP version this address is for.
*
* @return int IP version.
*/
public function getVersion(): int {
if($this->isV4())
return self::V4;
@ -45,9 +101,20 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
return self::UNKNOWN;
}
/**
* Checks whether this is an IPv4 address.
*
* @return bool true if IPv4.
*/
public function isV4(): bool {
return $this->width === 4;
}
/**
* Checks whether this is an IPv6 address.
*
* @return bool true if IPv6.
*/
public function isV6(): bool {
return $this->width === 16;
}
@ -60,6 +127,7 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
return $other instanceof IPAddress && $this->getRaw() === $other->getRaw();
}
#[\Override]
public function jsonSerialize(): mixed {
return inet_ntop($this->raw);
}
@ -67,15 +135,23 @@ final class IPAddress implements JsonSerializable, Stringable, IEquatable {
public function __serialize(): array {
return [$this->raw];
}
public function __unserialize(array $serialized): void {
$this->raw = $serialized[0];
$this->width = strlen($this->raw);
}
public static function parse(string $address): self {
$parsed = inet_pton($address);
/**
* Attempts to parse a string into an IP address.
*
* @param string $string String to parse.
* @throws InvalidArgumentException If $string does not contain a valid IP address string.
* @return IPAddress Representation of the given string.
*/
public static function parse(string $string): self {
$parsed = inet_pton($string);
if($parsed === false)
throw new InvalidArgumentException('$address is not a valid IP address.');
throw new InvalidArgumentException('$string is not a valid IP address.');
return new static($parsed);
}
}

View file

@ -1,7 +1,7 @@
<?php
// IPAddressRange.php
// Created: 2021-04-26
// Updated: 2023-01-01
// Updated: 2024-08-01
namespace Index\Net;
@ -10,23 +10,45 @@ use JsonSerializable;
use Stringable;
use Index\IEquatable;
/**
* Represents a CIDR range of IP addresses.
*/
final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
private IPAddress $base;
private int $mask;
/**
* @param IPAddress $base Base IP address.
* @param int $mask CIDR mask.
*/
public function __construct(IPAddress $base, int $mask) {
$this->base = $base;
$this->mask = $mask;
}
/**
* Retrieves the base IP address.
*
* @return IPAddress Base IP address.
*/
public function getBaseAddress(): IPAddress {
return $this->base;
}
/**
* Retrieves the CIDR mask.
*
* @return int CIDR mask.
*/
public function getMask(): int {
return $this->mask;
}
/**
* Retrieves full CIDR representation of this range.
*
* @return string CIDR string.
*/
public function getCIDR(): string {
return ((string)$this->base) . '/' . $this->getMask();
}
@ -41,6 +63,12 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
&& $this->base->equals($other->base);
}
/**
* Checks if a given IP address matches with this range.
*
* @param IPAddress $address IP address to check.
* @return bool true if the address matches.
*/
public function match(IPAddress $address): bool {
$width = $this->base->getWidth();
if($address->getWidth() !== $width)
@ -83,15 +111,23 @@ final class IPAddressRange implements JsonSerializable, Stringable, IEquatable {
'm' => $this->mask,
];
}
public function __unserialize(array $serialized): void {
$this->base = new IPAddress($serialized['b']);
$this->mask = $serialized['m'];
}
public static function parse(string $cidr): IPAddressRange {
$parts = explode('/', $cidr, 2);
/**
* Attempts to parse a string into an IP address range.
*
* @param string $string String to parse.
* @throws InvalidArgumentException If $string does not contain a valid CIDR range string.
* @return IPAddressRange Representation of the given string.
*/
public static function parse(string $string): IPAddressRange {
$parts = explode('/', $string, 2);
if(empty($parts[0]) || empty($parts[1]))
throw new InvalidArgumentException('$cidr is not a valid CIDR range.');
throw new InvalidArgumentException('$string is not a valid CIDR range.');
return new static(IPAddress::parse($parts[0]), (int)$parts[1]);
}
}

View file

@ -1,17 +1,25 @@
<?php
// IPEndPoint.php
// Created: 2021-04-30
// Updated: 2023-01-01
// Updated: 2024-08-01
namespace Index\Net;
use InvalidArgumentException;
use Index\XString;
/**
* Represents an IP address end point.
*/
class IPEndPoint extends EndPoint {
private IPAddress $address;
private int $port;
/**
* @param IPAddress $address IP address.
* @param int $port Network port, 0 to leave default.
* @throws InvalidArgumentException If $port is less than 0 or greater than 65535.
*/
public function __construct(IPAddress $address, int $port) {
if($port < 0 || $port > 0xFFFF)
throw new InvalidArgumentException('$port is not a valid port number.');
@ -19,14 +27,29 @@ class IPEndPoint extends EndPoint {
$this->port = $port;
}
/**
* Returns the IP address.
*
* @return IPAddress IP address.
*/
public function getAddress(): IPAddress {
return $this->address;
}
/**
* Whether a port is specified.
*
* @return bool true if the port number is greater than 0.
*/
public function hasPort(): bool {
return $this->port > 0;
}
/**
* Retrieves port number.
*
* @return int Network port number.
*/
public function getPort(): int {
return $this->port;
}
@ -34,6 +57,7 @@ class IPEndPoint extends EndPoint {
public function __serialize(): array {
return [$this->address, $this->port];
}
public function __unserialize(array $serialized): void {
$this->address = $serialized[0];
$this->port = $serialized[1];
@ -45,7 +69,14 @@ class IPEndPoint extends EndPoint {
&& $this->address->equals($other->address);
}
public static function parse(string $string): EndPoint {
/**
* Attempts to parse a string into an IP end point.
*
* @param string $string String to parse.
* @throws InvalidArgumentException If $string does not contain a valid IP end point string.
* @return IPEndPoint Representation of the given string.
*/
public static function parse(string $string): IPEndPoint {
if(str_starts_with($string, '[')) { // IPv6
$closing = strpos($string, ']');
if($closing === false)

View file

@ -1,17 +1,28 @@
<?php
// UnixEndPoint.php
// Created: 2021-04-30
// Updated: 2022-02-27
// Updated: 2024-08-01
namespace Index\Net;
/**
* Represents a UNIX socket path end point.
*/
class UnixEndPoint extends EndPoint {
private string $path;
/**
* @param string $socketPath Path to the socket file.
*/
public function __construct(string $socketPath) {
$this->path = $socketPath;
}
/**
* Gets path to the socket file.
*
* @return string Path to the socket file.
*/
public function getPath(): string {
return $this->path;
}
@ -19,6 +30,7 @@ class UnixEndPoint extends EndPoint {
public function __serialize(): array {
return [$this->path];
}
public function __unserialize(array $serialized): void {
$this->path = $serialized[0];
}
@ -28,13 +40,19 @@ class UnixEndPoint extends EndPoint {
&& $this->path === $other->path;
}
public static function parse(string $socketPath): UnixEndPoint {
if(str_starts_with($socketPath, 'unix:')) {
$uri = parse_url($socketPath);
$socketPath = $uri['path'] ?? '';
/**
* Attempts to parse a string into an UNIX end point.
*
* @param string $string String to parse.
* @return UnixEndPoint Representation of the given string.
*/
public static function parse(string $string): UnixEndPoint {
if(str_starts_with($string, 'unix:')) {
$uri = parse_url($string);
$string = $uri['path'] ?? '';
}
return new UnixEndPoint($socketPath);
return new UnixEndPoint($string);
}
public function __toString(): string {

View file

@ -1,19 +1,38 @@
<?php
// PerformanceCounter.php
// Created: 2022-02-16
// Updated: 2022-02-28
// Updated: 2024-08-01
namespace Index\Performance;
/**
* Represents a performance counter.
*/
final class PerformanceCounter {
/**
* Timer frequency in ticks per second.
*
* @return int|float Timer frequency.
*/
public static function getFrequency(): int|float {
return 1000000;
}
/**
* Current ticks from the system.
*
* @return int|float Ticks count.
*/
public static function getTicks(): int|float {
return hrtime(true);
}
/**
* Ticks elapsed since the given value.
*
* @param int|float $since Starting ticks value.
* @return int|float Ticks count since.
*/
public static function getTicksSince(int|float $since): int|float {
return self::getTicks() - $since;
}

View file

@ -1,7 +1,7 @@
<?php
// Stopwatch.php
// Created: 2021-04-26
// Updated: 2022-02-16
// Updated: 2024-08-01
namespace Index\Performance;
@ -17,11 +17,17 @@ class Stopwatch {
$this->frequency = PerformanceCounter::getFrequency();
}
/**
* Start time measurement.
*/
public function start(): void {
if($this->start < 0)
$this->start = PerformanceCounter::getTicks();
}
/**
* Stop time measurement.
*/
public function stop(): void {
if($this->start < 0)
return;

View file

@ -1,16 +1,25 @@
<?php
// TimingPoint.php
// Created: 2022-02-16
// Updated: 2022-02-28
// Updated: 2024-08-01
namespace Index\Performance;
/**
* Represents a timing point.
*/
class TimingPoint {
private int|float $timePoint;
private int|float $duration;
private string $name;
private string $comment;
/**
* @param int|float $timePoint Starting tick count.
* @param int|float $duration Duration ticks count.
* @param string $name Name of the timing point.
* @param string $comment Timing point comment.
*/
public function __construct(
int|float $timePoint,
int|float $duration,
@ -23,26 +32,56 @@ class TimingPoint {
$this->comment = $comment;
}
/**
* Gets the starting ticks count.
*
* @return int|float Starting ticks count.
*/
public function getTimePointTicks(): int|float {
return $this->timePoint;
}
/**
* Gets the duration ticks count.
*
* @return int|float Duration ticks count.
*/
public function getDurationTicks(): int|float {
return $this->duration;
}
/**
* Gets the duration time amount.
*
* @return float Duration time amount.
*/
public function getDurationTime(): float {
return $this->duration / PerformanceCounter::getFrequency();
}
/**
* Gets time point name.
*
* @return string Time point name.
*/
public function getName(): string {
return $this->name;
}
/**
* Checks whether time point has a comment.
*
* @return bool true if the time point has a comment.
*/
public function hasComment(): bool {
return $this->comment !== '';
}
/**
* Gets time point comment.
*
* @return string Time point comment.
*/
public function getComment(): string {
return $this->comment;
}

View file

@ -1,45 +1,79 @@
<?php
// Timings.php
// Created: 2022-02-16
// Updated: 2022-02-16
// Updated: 2024-08-01
namespace Index\Performance;
use InvalidArgumentException;
/**
* Represents a Stopwatch with timing points.
*/
class Timings {
private Stopwatch $sw;
private int|float $lastLapTicks;
private array $laps;
/**
* @param ?Stopwatch $sw Stopwatch to measure timing points from, null to start a new one.
*/
public function __construct(?Stopwatch $sw = null) {
$this->sw = $sw ?? Stopwatch::startNew();
$this->lastLapTicks = $this->sw->getElapsedTicks();
}
/**
* Gets the underlying stopwatch object.
*
* @return Stopwatch Stopwatch object.
*/
public function getStopwatch(): Stopwatch {
return $this->sw;
}
/**
* Returns timing points.
*
* @return TimingPoint[] Timing points.
*/
public function getLaps(): array {
return $this->laps;
}
/**
* Starts the underlying stopwatch.
*/
public function start(): void {
$this->sw->start();
}
/**
* Stops the underlying stopwatch.
*/
public function stop(): void {
$this->sw->stop();
}
/**
* Checks whether the underlying stopwatch is running.
*
* @return bool true if it is running.
*/
public function isRunning(): bool {
return $this->sw->isRunning();
}
/**
* Record a timing point.
*
* @param string $name Timing point name, must be alphanumeric.
* @param string $comment Timing point comment.
* @throws InvalidArgumentException If $name is not alphanumeric.
*/
public function lap(string $name, string $comment = ''): void {
if(!ctype_alnum($name))
throw new InvalidArgumentException('$name must be alpha-numeric.');
throw new InvalidArgumentException('$name must be alphanumeric.');
$lapTicks = $this->sw->getElapsedTicks();
$elapsed = $lapTicks - $this->lastLapTicks;

View file

@ -1,7 +1,7 @@
<?php
// XDateTime.php
// Created: 2024-07-31
// Updated: 2024-07-31
// Updated: 2024-08-01
namespace Index;
@ -9,6 +9,9 @@ use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
/**
* Provides a set of DateTime related utilities.
*/
final class XDateTime {
private const COMPARE_FORMAT = 'YmdHisu';

View file

@ -1,7 +1,7 @@
<?php
// BencodeTest.php
// Created: 2023-07-21
// Updated: 2024-07-31
// Updated: 2024-08-01
declare(strict_types=1);
@ -29,7 +29,7 @@ final class BencodeTest extends TestCase {
$this->assertArrayHasKey('private', $decoded['info']);
$this->assertEquals(1, $decoded['info']['private']);
$decoded = Bencode::decode(base64_decode(self::TORRENT), dictAsObject: true);
$decoded = Bencode::decode(base64_decode(self::TORRENT), associative: false);
$this->assertEquals('https://tracker.flashii.net/announce.php/meow', $decoded->announce);
$this->assertEquals(1689973664, $decoded->{'creation date'});