Imported into new repository.

This commit is contained in:
flash 2022-09-13 15:13:11 +02:00
commit ac2255d24d
187 changed files with 15021 additions and 0 deletions

5
.gitattributes vendored Normal file
View File

@ -0,0 +1,5 @@
* text=auto
*.sh text eol=lf
*.php text eol=lf
*.bat text eol=crlf
VERSION text eol=lf

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
[Tt]humbs.db
[Dd]esktop.ini
.DS_Store
.vscode/
.vs/
.idea/
docs/html/
.phpdoc*
.phpunit*

25
LICENCE Normal file
View File

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2021-2022, flashwave <me@flash.moe>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

39
README.md Normal file
View File

@ -0,0 +1,39 @@
# Index
Index is a common library for my PHP projects.
It provides a number of components that I would otherwise copy between projects (and thus become out-of-sync) as well as a number of quality of life things on top of standard PHP stdlib functionality such as abstractions of arrays and strings as objects inspired by .NET's standard library.
## Requirements and Dependencies
Index currently targets **PHP 8.1**. (also list extensions!!!!!!!!!)
### `Index\Data\MariaDB`
Requires the `mysqli` extension. `mysqlnd` is recommended as the underlying driver, but `libmysql` should work without a hitch. This driver also works for MySQL as the dependencies would suggest, but you should consider using MariaDB instead of possible.
### `Index\Data\SQLite`
Requires the `sqlite3` extension.
## Versioning
Index versioning will mostly follow the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html), counting dropped support for a minor PHP version (e.g. 7.1 -> 7.2 or 7.4 -> 8.0) as a reason to increment the major version.
Previous major versions may be supported for a time with backports depending on what projects of mine still target older versions of PHP.
The version is stored in the root of the repository in a file called `VERSION` and can be read out within Index using `Index\Environment::getIndexVersion()`.
## Contribution
By submitting code for inclusion in the main Index source tree you agree to transfer ownership of the code to the project owner.
The contributor will still be attributed for the contributed code, unless they ask for this attribution to be removed.
This is to avoid intellectual property rights traps and drama that could lead to blackmail situations.
If you do not agree with these terms, you are free to fork off.
## Licencing
Index is available under the BSD 2-Clause License, a full version of which is enclosed in the LICENCE file.

37
TODO.md Normal file
View File

@ -0,0 +1,37 @@
# TODO
## High Prio
- Create tests for everything testable.
- Draw out a plan for the templating backend.
- Will probably just construct using the HTML abstraction stuff, would make AJAX'ing page updates trivial also.
- Reinvestigate the semi-isolated PHP script template system I used in some other project, I forgot which.
- Determine structure for markup parsing abstraction.
- Writing a bbcode and markdown parser outside of Index, will implement a structure when done.
- Create HTML construction utilities.
- Create similarly address GD2/Imagick wrappers.
- Review CSRF code, Something about it has me constantly wondering if its too Complex.
- Figure out the SQL Query builder.
- Might make sense to just have a predicate builder and selector builder.
- Create a URL formatter.
- Add RSS/Atom feed construction utilities.
- Probably get rid of StringBuilder, PHP strings aren't entirely immutable so that whole issue doesn't even exist.
I have a number of possible fixes in my head for this. The primary reason for A/W/IString was to have a consistent API between
normal strings and mbstrings. I'm not sure yet but part of me wants to keep IString but move a lot of functionality into XString and make that the go-to. WString could remain, maybe in a reduced form? Having the encoding of the string associated with it is very convenient.
## Low Prio
- Get guides working on phpdoc.
- Create phpdoc template.
- Review all constructors and see which one should be marked @internal.

1
VERSION Normal file
View File

@ -0,0 +1 @@
0.2202.281831

17
bench/_init.php Normal file
View File

@ -0,0 +1,17 @@
<?php
require_once __DIR__ . '/../index.php';
function bench(string $name, int $times, callable $body): void {
printf('Running "%s" %d times%s', $name, $times, PHP_EOL);
$sw = \Index\Performance\Stopwatch::startNew();
while(--$times > 0)
$body();
$sw->stop();
printf('Took: %Fms %s', $sw->getElapsedTime(), PHP_EOL);
echo PHP_EOL;
}

7
bench/example.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require_once __DIR__ . '/_init.php';
bench('Example name', 100000, function() {
// run something here
$i = 1 + 1;
});

11
docs/index.rst Normal file
View File

@ -0,0 +1,11 @@
.. meta::
:layout: landingpage
:tada: true
Index Documentation
===================
No Markdown!
------------
test RST file because phpdoc doesn't into markdown

24
index.php Normal file
View File

@ -0,0 +1,24 @@
<?php
// index.php
// Created: 2021-04-26
// Updated: 2021-05-04
namespace Index;
define('NDX_ROOT', __DIR__);
define('NDX_DIR_SRC', NDX_ROOT . DIRECTORY_SEPARATOR . 'src');
require_once NDX_DIR_SRC . DIRECTORY_SEPARATOR . 'Autoloader.php';
Autoloader::addNamespace(__NAMESPACE__, NDX_DIR_SRC);
Autoloader::register();
// currently phpstan sucks and relies on error suppression, luckily it leaves a constant!
if(!defined('__PHPSTAN_RUNNING__')) {
// defining this WILL cause issues, never do it unless you HAVE to
if(!defined('NDX_LEAVE_ERRORS'))
Exceptions::convertErrors();
if(!defined('NDX_LEAVE_EXCEPTIONS'))
Exceptions::handleExceptions();
}

41
phpdoc.xml Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8" ?>
<phpdocumentor configVersion="3.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://www.phpdoc.org"
xsi:noNamespaceSchemaLocation="data/xsd/phpdoc.xsd">
<title>Index Documentation</title>
<template name="default"/>
<paths>
<output>docs/html</output>
</paths>
<version number="0.1">
<folder>latest</folder>
<api format="php">
<output>api</output>
<visibility>public</visibility>
<default-package-name>Index</default-package-name>
<source dsn=".">
<path>src</path>
</source>
<extensions>
<extension>php</extension>
</extensions>
<ignore hidden="true" symlinks="true">
<path>tests/**/*</path>
</ignore>
<ignore-tags>
<ignore-tag>template</ignore-tag>
<ignore-tag>template-extends</ignore-tag>
<ignore-tag>template-implements</ignore-tag>
<ignore-tag>extends</ignore-tag>
<ignore-tag>implements</ignore-tag>
</ignore-tags>
</api>
<guide format="rst">
<source dsn=".">
<path>docs</path>
</source>
<output>guide</output>
</guide>
</version>
</phpdocumentor>

6
phpstan.neon Normal file
View File

@ -0,0 +1,6 @@
parameters:
level: 5 # Raise this eventually
paths:
- src
bootstrapFiles:
- index.php

27
phpunit.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.5/phpunit.xsd"
bootstrap="index.php"
colors="true"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="true"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory suffix="Test.php">tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>

274
src/AString.php Normal file
View File

@ -0,0 +1,274 @@
<?php
// AString.php
// Created: 2021-04-26
// Updated: 2022-02-27
namespace Index;
use Traversable;
use InvalidArgumentException;
/**
* Provides an immutable ASCII string with arrow methods.
*
* Normal PHP strings should be used for buffers/byte arrays.
*/
final class AString implements IString {
use XStringTrait;
private string $value;
/**
* Create an AString instance.
*
* @param string $value PHP string to inherit.
* @return AString New instance of AString.
*/
public function __construct(string $value) {
$this->value = $value;
}
public function getLength(): int {
return strlen($this->value);
}
public function isEmpty(): bool {
return $this->value === '';
}
public function __toString(): string {
return $this->value;
}
/**
* Checks if an offset exists in the string.
*
* You should call isset($string[$offset]) instead of $string->offsetExists($offset).
*
* @see https://www.php.net/manual/en/arrayaccess.offsetexists.php
* @param int $offset Character offset.
* @return bool true if it exists, false if not.
*/
public function offsetExists(mixed $offset): bool {
return isset($this->value[$offset]);
}
/**
* Gets an offset from the string.
*
* You should do $string[$offset] instead of $string->offsetGet($offset).
*
* @see https://www.php.net/manual/en/arrayaccess.offsetget.php
* @param int $offset Character offset.
* @return string Character at that offset.
*/
public function offsetGet(mixed $offset): mixed {
return $this->value[$offset];
}
/**
* Gets an iterator object for this string.
*
* @return StringIterator An iterator for this string.
*/
public function getIterator(): Traversable {
return new StringIterator($this);
}
/**
* Returns the data which should be serialized as json.
*
* @see https://www.php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed Data to be passed to json_encode.
*/
public function jsonSerialize(): mixed {
return $this->value;
}
public function bencodeSerialise(): mixed {
return $this->value;
}
/**
* Gets a serialized representation of this object.
*
* @return array Serialized data.
*/
public function __serialize(): array {
return [$this->value];
}
/**
* Reconstructs an object from a serialized string.
*
* @param array $serialized Serialized data.
*/
public function __unserialize(array $serialized): void {
$this->value = $serialized[0];
}
/**
* Checks whether this string is identical to another.
*
* @param mixed $other An instance of AString or a PHP string.
* @return bool true if the strings have the same value, false if not.
*/
public function equals(mixed $other): bool {
return $this->compare($other) === 0;
}
/**
* Compares whether this string is identical to another.
*
* @param mixed $other An instance of IString or a PHP string.
*/
public function compare(mixed $other): int {
return strcmp($this->value, (string)$other);
}
/**
* Creates a new identical AString instance.
*
* This method is somewhat pointless, given the immutable nature of this object,
* but rather people calling this instead of calling ->substring(0);
*
* @return AString A new identical instance of AString.
*/
public function clone(): mixed {
return new AString($this->value);
}
public function indexOf(IString|string $text, int $offset = 0): int {
$pos = strpos($this->value, (string)$text, $offset);
if($pos === false)
return -1;
return $pos;
}
public function contains(IString|string $text): bool {
return str_contains($this->value, (string)$text);
}
public function substring(int $offset, int|null $length = null): IString {
return new AString(substr($this->value, $offset, $length));
}
public function replace(IString|string $search, IString|string $replace): IString {
return new AString(str_replace((string)$search, (string)$replace, $this->value));
}
public function append(IString|string $string): IString {
return new AString($this->value . (string)$string);
}
public function prepend(IString|string $string): IString {
return new AString(((string)$string) . $this->value);
}
public function split(IString|string $separator, int $limit = PHP_INT_MAX): array {
$separator = (string)$separator;
if(empty($separator))
throw new InvalidArgumentException('$separator may not be empty.');
return XArray::select(
explode($separator, $this->value, $limit),
fn($str) => new AString($str)
);
}
public function chunk(int $chunkSize): array {
return XArray::select(
str_split($this->value, $chunkSize),
fn($str) => new AString($str)
);
}
public function trim(IString|string $characters = IString::TRIM_CHARS): IString {
return new AString(trim($this->value, (string)$characters));
}
public function trimStart(IString|string $characters = IString::TRIM_CHARS): IString {
return new AString(ltrim($this->value, (string)$characters));
}
public function trimEnd(IString|string $characters = IString::TRIM_CHARS): IString {
return new AString(rtrim($this->value, (string)$characters));
}
public function toLower(): IString {
return new AString(strtolower($this->value));
}
public function toUpper(): IString {
return new AString(strtoupper($this->value));
}
public function reverse(): IString {
return new AString(strrev($this->value));
}
public function startsWith(IString|string $text): bool {
return str_starts_with($this->value, (string)$text);
}
public function endsWith(IString|string $text): bool {
return str_ends_with($this->value, (string)$text);
}
/**
* Casts this AString to a WString.
*
* @param ?string $encoding Intended encoding, null for the Index-level default.
* @param bool $convert true to convert the string to the target encoding, false to leave the bytes as-is.
* @return WString A WString of the provided encoding with the value of this AString.
*/
public function toWString(?string $encoding = null, bool $convert = true): WString {
$value = $this->value;
$encoding ??= WString::getDefaultEncoding();
if($convert)
$value = mb_convert_encoding($value, $encoding, 'ascii');
return new WString($value, $encoding);
}
/**
* Joins an iterable object together with a separator to create a string.
*
* @param iterable $source Source object.
* @param IString|string $separator Separator to use as glue.
* @return AString Resulting string.
*/
public static function join(iterable $source, IString|string $separator = ''): AString {
if(!is_array($source)) {
$parts = [];
foreach($source as $value)
$parts[] = $value;
$source = $parts;
}
return new AString(implode((string)$separator, $source));
}
/**
* Returns a reusable empty string instance.
*
* @return AString An empty string.
*/
public static function empty(): AString {
static $empty = null;
$empty ??= new AString('');
return $empty;
}
/**
* Converts a value to AString.
*
* @param mixed $value Source value.
* @return AString An AString representing the given value.
*/
public static function cast(mixed $value): AString {
if($value instanceof AString)
return $value;
if($value instanceof WString)
return $value->toAString();
return new AString(strval($value));
}
}

108
src/Autoloader.php Normal file
View File

@ -0,0 +1,108 @@
<?php
// Autoloader.php
// Created: 2021-05-04
// Updated: 2021-05-12
namespace Index;
use InvalidArgumentException;
/**
* Provides a simple PSR-4 style autoloader.
*
* Only basic types should be used in this class because this is the first file included
* and obviously can't autoload things before it has been set up.
*/
final class Autoloader {
private const EXTENSION = '.php';
private static array $namespaces = [];
/**
* Registers this autoloader with PHP.
*/
public static function register(): void {
spl_autoload_register([self::class, 'autoloader']);
}
/**
* Unregistered this autoloader with PHP.
*/
public static function unregister(): void {
spl_autoload_unregister([self::class, 'autoloader']);
}
/**
* Cleans a PHP class path for file system look up.
*
* @param string $name A PHP class path.
* @return string A cleaned PHP class path.
*/
public static function cleanName(string $name): string {
return trim($name, '\\');
}
/**
* Tries to find a class in the registered namespaces directories.
*
* Only supports the .php extension, others are silly and only add overhead.
*
* @param string $className Target class path.
*/
public static function autoloader(string $className): void {
$classPath = explode('\\', self::cleanName($className));
for($i = 0; $i < count($classPath); ++$i) {
$rootSpace = implode('\\', array_slice($classPath, 0, $i + 1));
if(isset(self::$namespaces[$rootSpace])) {
$path = self::$namespaces[$rootSpace]
. DIRECTORY_SEPARATOR
. implode(DIRECTORY_SEPARATOR, array_slice($classPath, $i + 1))
. self::EXTENSION;
if(is_file($path)) {
require_once $path;
return;
}
}
}
}
/**
* Registers a directory with a namespace. Projects making use of Index may use this.
*
* @param string $namespace Target namespace.
* @param string $directory Directory containing the classes of this namespace.
* @throws InvalidArgumentException if $namespace is an empty string.
* @throws InvalidArgumentException if $directory is a non-existent directory.
* @throws InvalidArgumentException if $namespace is already registered.
*/
public static function addNamespace(string $namespace, string $directory): void {
if(empty($namespace))
throw new InvalidArgumentException('$namespace may not be an empty string.');
if(!is_dir($directory))
throw new InvalidArgumentException('$directory must point to an existing directory.');
$namespace = self::cleanName($namespace);
$directory = rtrim(realpath($directory), DIRECTORY_SEPARATOR);
if(isset(self::$namespaces[$namespace]))
throw new InvalidArgumentException("{$namespace} is already a registered namespace.");
self::$namespaces[$namespace] = $directory;
}
/**
* Removes a registered namespace.
*
* Attempts to unregister Index are ignored.
*
* @param string $namespace Namespace to be removed.
*/
public static function removeNamespace(string $namespace): void {
$namespace = self::cleanName($namespace);
if($namespace !== 'Index')
unset(self::$namespaces[$namespace]);
}
}

View File

@ -0,0 +1,37 @@
<?php
// ArrayIterator.php
// Created: 2022-02-03
// Updated: 2022-02-03
namespace Index\Collections;
use Iterator;
class ArrayIterator implements Iterator {
private array $array;
private bool $wasValid = true;
public function __construct(array $array) {
$this->array = $array;
}
public function current(): mixed {
return current($this->array);
}
public function key(): mixed {
return key($this->array);
}
public function next(): void {
$this->wasValid = next($this->array) !== false;
}
public function rewind(): void {
$this->wasValid = reset($this->array) !== false;
}
public function valid(): bool {
return $this->wasValid;
}
}

View File

@ -0,0 +1,10 @@
<?php
// IArrayable.php
// Created: 2022-02-03
// Updated: 2022-02-03
namespace Index\Collections;
interface IArrayable {
function toArray(): array;
}

17
src/Colour.php Normal file
View File

@ -0,0 +1,17 @@
<?php
// Colour.php
// Created: 2021-09-09
// Updated: 2022-02-02
namespace Index;
abstract class Colour extends XObject {
abstract public function getRaw(): int;
abstract public function getRed(): int;
abstract public function getGreen(): int;
abstract public function getBlue(): int;
public function getAlpha(): float {
return 1.0;
}
}

26
src/ColourARGB.php Normal file
View File

@ -0,0 +1,26 @@
<?php
// ColourARGB.php
// Created: 2021-09-08
// Updated: 2022-01-03
namespace Index;
class ColourARGB extends ColourRGB {
public function getAlphaRaw(): int {
return ($this->raw >> 24) & 0xFF;
}
public function getAlpha(): float {
return ((float)$this->getAlphaRaw() / 0xFF);
}
public function toString(): IString {
return new AString(sprintf(
'rgba(%d,%d,%d,%F)',
$this->getRed(),
$this->getGreen(),
$this->getBlue(),
round($this->getAlpha(), 3)
));
}
}

25
src/ColourLegacy.php Normal file
View File

@ -0,0 +1,25 @@
<?php
// ColourLegacy.php
// Created: 2021-09-09
// Updated: 2022-01-20
namespace Index;
class ColourLegacy extends ColourRGB {
private const INHERIT = 0x40000000;
public function shouldInherit(): bool {
return ($this->raw & self::INHERIT) > 0;
}
public function toString(): IString {
if($this->shouldInherit()) {
static $inherit = null;
if($inherit === null)
$inherit = new AString('inherit');
return $inherit;
}
return parent::toString();
}
}

34
src/ColourRGB.php Normal file
View File

@ -0,0 +1,34 @@
<?php
// ColourRGB.php
// Created: 2021-09-09
// Updated: 2021-09-09
namespace Index;
class ColourRGB extends Colour {
protected int $raw;
public function __construct(int $raw) {
$this->raw = $raw;
}
public function getRaw(): int {
return $this->raw;
}
public function getRed(): int {
return ($this->raw >> 16) & 0xFF;
}
public function getGreen(): int {
return ($this->raw >> 8) & 0xFF;
}
public function getBlue(): int {
return $this->raw & 0xFF;
}
public function toString(): IString {
return new AString('#' . str_pad(dechex($this->raw & 0xFFFFFF), 6, '0', STR_PAD_LEFT));
}
}

View File

@ -0,0 +1,11 @@
<?php
// BeginTransactionFailedException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when transaction start fails.
*/
class BeginTransactionFailedException extends TransactionException {}

View File

@ -0,0 +1,11 @@
<?php
// CommitFailedException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when a transaction commit fails.
*/
class CommitFailedException extends TransactionException {}

View File

@ -0,0 +1,11 @@
<?php
// ConnectionFailedException.php
// Created: 2022-01-29
// Updated: 2022-02-02
namespace Index\Data;
/**
* Exception to be thrown when a connection fails.
*/
class ConnectionFailedException extends DataException {}

View File

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

101
src/Data/DbTools.php Normal file
View File

@ -0,0 +1,101 @@
<?php
// DbTools.php
// Created: 2021-05-02
// Updated: 2022-02-28
namespace Index\Data;
use InvalidArgumentException;
use Index\Type;
/**
* Common database actions.
*/
final class DbTools {
private const DB_PROTOS = [
'null' => NullDb\NullDbBackend::class,
'mariadb' => MariaDB\MariaDBBackend::class,
'mysql' => MariaDB\MariaDBBackend::class,
'sqlite' => SQLite\SQLiteBackend::class,
'sqlite3' => SQLite\SQLiteBackend::class,
];
public static function create(string $dsn): IDbConnection {
static $backends = [];
$uri = parse_url($dsn);
if($uri === false)
throw new InvalidArgumentException('$dsn is not a valid uri.');
$scheme = $uri['scheme'];
if(in_array($scheme, $backends))
$backend = $backends[$scheme];
else {
$backend = null;
if(array_key_exists($scheme, self::DB_PROTOS))
$name = self::DB_PROTOS[$scheme];
else
$name = str_replace('-', '\\', $scheme);
if(class_exists($name) && is_subclass_of($name, IDbBackend::class)) {
$backend = new $name;
$name = get_class($backend);
}
if($backend === null)
throw new DataException('No implementation is available for the specified scheme.');
if(!$backend->isAvailable())
throw new DataException('Requested database backend is not available, likely due to missing dependencies.');
$backends[$name] = $backend;
}
return $backend->createConnection(
$backend->parseDsn($uri)
);
}
/**
* Transaction wrapper.
*
* Takes a database connection with transaction support and a callable that may return a boolean based on the success of the actions.
* If the callable returns nothing, nothing will happen.
* If the callable returns true, commit will be called.
* If the callable returns false, rollback will be called.
*
* @param IDbTransactions $connection A database connection with transaction support.
* @param callable $callable A callable that handles the transaction, may return a bool.
*/
public static function transaction(IDbTransactions $connection, callable $callable): void {
$connection->beginTransaction();
$result = $callable($connection) ?? null;
if(is_bool($result)) {
if($result)
$connection->commit();
else
$connection->rollback();
}
}
/**
* Detects the DbType of the passed argument. Should be used for DbType::AUTO.
*
* @param mixed $value A value of unknown type.
* @return int DbType of the value passed in the argument.
*/
public static function detectType(mixed $value): int {
if(is_null($value))
return DbType::NULL;
if(is_float($value))
return DbType::FLOAT;
if(is_int($value))
return DbType::INTEGER;
// ┌ should probably also check for Stringable, length should also be taken into consideration
// ↓ though maybe with that it's better to assume that when an object is passed it'll always be Massive
if(is_string($value))
return DbType::STRING;
return DbType::BLOB;
}
}

53
src/Data/DbType.php Normal file
View File

@ -0,0 +1,53 @@
<?php
// DbType.php
// Created: 2021-05-02
// Updated: 2021-05-04
namespace Index\Data;
/**
* Map of common database types.
*/
final class DbType {
/**
* Automatically detect the type. Should be used in combination with DbTools::detectType.
*
* @var int
*/
public const AUTO = 0;
/**
* Represents a NULL value. If this type is specified, the value it was associated with should be overriden with NULL.
*
* @var int
*/
public const NULL = 1;
/**
* An integer type.
*
* @var int
*/
public const INTEGER = 2;
/**
* A double precision floating point.
*
* @var int
*/
public const FLOAT = 3;
/**
* A textual string.
*
* @var int
*/
public const STRING = 4;
/**
* Binary blob data.
*
* @var int
*/
public const BLOB = 5;
}

35
src/Data/IDbBackend.php Normal file
View File

@ -0,0 +1,35 @@
<?php
// IDbBackend.php
// Created: 2021-04-30
// Updated: 2022-02-28
namespace Index\Data;
/**
* Information about a database layer. Should not have any external dependencies.
*/
interface IDbBackend {
/**
* Checks whether the driver is available and a connection can be made.
*
* @return bool If true a connection can be made, if false a required extension is missing.
*/
function isAvailable(): bool;
/**
* Creates a connection with the database described in the argument.
*
* @param IDbConnectionInfo $connectionInfo Object that describes the desired connection.
* @throws \InvalidArgumentException An invalid implementation of IDbConnectionInfo was provided.
* @return IDbConnection A connection described in the connection info.
*/
function createConnection(IDbConnectionInfo $connectionInfo): IDbConnection;
/**
* Constructs a connection info instance from a dsn.
*
* @param string|array $dsn DSN with connection information.
* @return IDbConnectionInfo Connection info based on the dsn.
*/
function parseDsn(string|array $dsn): IDbConnectionInfo;
}

View File

@ -0,0 +1,49 @@
<?php
// IDbConnection.php
// Created: 2021-04-30
// Updated: 2022-02-27
namespace Index\Data;
use Index\ICloseable;
/**
* Represents a connection to a database service.
*/
interface IDbConnection extends ICloseable {
/**
* Returns the ID of the last inserted row.
*
* @return int|string Last inserted ID.
*/
function getLastInsertId(): int|string;
/**
* Prepares a statement for execution and returns a database statement instance.
*
* The statement should use question mark (?) parameters.
*
* @param string $query SQL query to prepare.
* @return IDbStatement An instance of an implementation of IDbStatement.
*/
function prepare(string $query): IDbStatement;
/**
* Executes a statement and returns a database result instance.
*
* @param string $query SQL query to execute.
* @return IDbResult An instance of an implementation of IDbResult
*/
function query(string $query): IDbResult;
/**
* Executes a statement and returns how many rows are affected.
*
* Does not request results back from the database and thus should have better
* performance if the consumer doesn't care about them.
*
* @param string $query SQL query to execute.
* @return int|string Number of rows affected by the query.
*/
function execute(string $query): int|string;
}

View File

@ -0,0 +1,13 @@
<?php
// IDbConnectionInfo.php
// Created: 2021-04-30
// Updated: 2022-02-16
namespace Index\Data;
/**
* Base type for database connection info.
*
* Any database backend should have its own implementation of this, there are no baseline requirements.
*/
interface IDbConnectionInfo {}

88
src/Data/IDbResult.php Normal file
View File

@ -0,0 +1,88 @@
<?php
// IDbResult.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data;
use Index\AString;
use Index\ICloseable;
use Index\WString;
use Index\IO\Stream;
/**
* Represents a database result set.
*/
interface IDbResult extends ICloseable {
/**
* Fetches the next result set.
*
* @return bool true if the result set was loaded, false if no more results are available.
*/
function next(): bool;
/**
* Checks if a given index has a NULL value.
*
* @param int|string $index Target index.
* @return bool true if the value is null, false if not.
*/
function isNull(int|string $index): bool;
/**
* Gets the value from the target index without any additional casting.
*
* @param int|string $index Target index.
* @return mixed Target value.
*/
function getValue(int|string $index): mixed;
/**
* Gets the value from the target index cast as a native string.
*
* @param int|string $index Target index.
* @return string Returns a string of the value.
*/
function getString(int|string $index): string;
/**
* Gets the value from the target index cast as an ASCII string.
*
* @param int|string $index Target index.
* @return AString Returns an AString of the value.
*/
function getAString(int|string $index): AString;
/**
* Gets the value from the target index cast as a multi-byte string.
*
* @param int|string $index Target index.
* @param string $encoding Encoding of the string.
* @return WString Returns an WString of the value.
*/
function getWString(int|string $index, string $encoding): WString;
/**
* Gets the value from the target index cast as an integer.
*
* @param int|string $index Target index.
* @return int Returns the value cast to an integer.
*/
function getInteger(int|string $index): int;
/**
* Gets the value from the target index cast as a floating point number.
*
* @param int|string $index Target index.
* @return float Returns the value cast to a floating point number.
*/
function getFloat(int|string $index): float;
/**
* Gets the value from the target index as a Stream.
*
* @param int|string $index Target index.
* @return ?Stream A Stream if data is available, null if not.
*/
function getStream(int|string $index): ?Stream;
}

53
src/Data/IDbStatement.php Normal file
View File

@ -0,0 +1,53 @@
<?php
// IDbStatement.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data;
use Index\ICloseable;
/**
* Represents a prepared database statement.
*/
interface IDbStatement extends ICloseable {
/**
* Returns how many parameters there are.
*
* @return int Number of parameters.
*/
function getParameterCount(): int;
/**
* Assigns a value to a parameter.
*
* @param int $ordinal Index of the target parameter.
* @param mixed $value Value to assign to the parameter.
* @param int $type Type of the value, if left to DbType::AUTO DbTools::detectType will be used on $value.
*/
function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void;
/**
* Gets the result after execution.
*
* @return IDbResult Instance of an implementation of IDbResult.
*/
function getResult(): IDbResult;
/**
* Returns the ID of the last inserted row.
*
* @return int|string Last inserted ID.
*/
function getLastInsertId(): int|string;
/**
* Executes this statement.
*/
function execute(): void;
/**
* Resets this statement for reuse.
*/
function reset(): void;
}

View File

@ -0,0 +1,57 @@
<?php
// IDbTransactions.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data;
/**
* Indicates supports for transactions in a database connection.
*/
interface IDbTransactions extends IDbConnection {
/**
* Sets whether changes should be applied immediately or whether commit should always be called first.
*
* @param bool $state true if things should automatically be committed, false if not.
*/
function setAutoCommit(bool $state): void;
/**
* Enters a transaction.
*
* @throws BeginTransactionFailedException If the creation of the transaction failed.
*/
function beginTransaction(): void;
/**
* Commits the actions done during a transaction and ends the transaction.
* A new transaction will be started if auto-commit is disabled.
*
* @throws CommitFailedException If the commit failed.
*/
function commit(): void;
/**
* Rolls back to the state before a transaction start or to a specified save point.
*
* @param ?string $name Name of the save point, null for the entire transaction.
* @throws RollbackFailedException If rollback failed.
*/
function rollback(?string $name = null): void;
/**
* Creates a save point in the transaction that can be rolled back to.
*
* @param string $name Name for the save point.
* @throws SavePointFailedException If save point creation failed.
*/
function savePoint(string $name): void;
/**
* Releases a save point.
*
* @param string $name Name of the save point.
* @throws ReleaseSavePointFailedException If releasing the save point failed.
*/
function releaseSavePoint(string $name): void;
}

View File

@ -0,0 +1,122 @@
<?php
// MariaDBBackend.php
// Created: 2021-04-30
// Updated: 2022-02-28
namespace Index\Data\MariaDB;
use InvalidArgumentException;
use Index\Version;
use Index\Data\IDbBackend;
use Index\Data\IDbConnection;
use Index\Data\IDbConnectionInfo;
use Index\Net\EndPoint;
use Index\Net\UnixEndPoint;
/**
* Information about the MariaDB/MySQL database layer.
*/
class MariaDBBackend implements IDbBackend {
public function isAvailable(): bool {
return extension_loaded('mysqli');
}
/**
* @internal
*/
public static function intToVersion(int $version): Version {
$sub = $version % 100;
$version = floor($version / 100);
$minor = $version % 100;
$version = floor($version / 100);
$major = $version % 100;
return new Version($major, $minor, $sub);
}
/**
* Gets the version of the underlying client library.
*
* @return Version Version of the client library.
*/
public function getClientVersion(): Version {
return self::intToVersion(mysqli_get_client_version());
}
/**
* Creates a connection with a MariaDB or MySQL server.
*
* @param MariaDBConnectionInfo $connectionInfo Object that describes the desired connection.
* @return MariaDBConnection A connection with a MariaDB or MySQL server.
*/
public function createConnection(IDbConnectionInfo $connectionInfo): IDbConnection {
if(!($connectionInfo instanceof MariaDBConnectionInfo))
throw new InvalidArgumentException('$connectionInfo must by of type MariaDBConnectionInfo');
return new MariaDBConnection($connectionInfo);
}
/**
* @return MariaDBConnectionInfo MariaDB connection info.
*/
public function parseDsn(string|array $dsn): IDbConnectionInfo {
if(is_string($dsn)) {
$dsn = parse_url($dsn);
if($dsn === false)
throw new InvalidArgumentException('$dsn is not a valid uri.');
}
if(!isset($dsn['host']))
throw new InvalidArgumentException('Host is missing from DSN.');
if(!isset($dsn['path']))
throw new InvalidArgumentException('Path is missing from DSN.');
$host = $dsn['host'];
$needsUnix = $host === ':unix';
if(!$needsUnix && isset($dsn['port']))
$host .= ':' . $dsn['port'];
$user = $dsn['user'] ?? '';
$pass = $dsn['pass'] ?? '';
$endPoint = $needsUnix ? null : EndPoint::parse($host);
$dbName = str_replace('/', '_', trim($dsn['path'], '/')); // cute for table prefixes i think
if(!isset($dsn['query'])) {
$charSet = null;
$initCommand = null;
$keyPath = null;
$certPath = null;
$certAuthPath = null;
$trustedCertsPath = null;
$cipherAlgos = null;
$verifyCert = false;
$useCompression = false;
} else {
parse_str(str_replace('+', '%2B', $dsn['query']), $query);
$unixPath = $query['socket'] ?? null;
$charSet = $query['charset'] ?? null;
$initCommand = $query['init'] ?? null;
$keyPath = $query['enc_key'] ?? null;
$certPath = $query['enc_cert'] ?? null;
$certAuthPath = $query['enc_authority'] ?? null;
$trustedCertsPath = $query['enc_trusted_certs'] ?? null;
$cipherAlgos = $query['enc_ciphers'] ?? null;
$verifyCert = !empty($query['enc_verify']);
$useCompression = !empty($query['compress']);
}
if($needsUnix) {
if($unixPath === null)
throw new InvalidArgumentException('Unix socket path is missing from DSN.');
$endPoint = new UnixEndPoint($unixPath);
}
return new MariaDBConnectionInfo(
$endPoint, $user, $pass, $dbName,
$charSet, $initCommand, $keyPath, $certPath,
$certAuthPath, $trustedCertsPath, $cipherAlgos,
$verifyCert, $useCompression
);
}
}

View File

@ -0,0 +1,93 @@
<?php
// MariaDBCharacterSetInfo.php
// Created: 2021-05-02
// Updated: 2022-02-02
namespace Index\Data\MariaDB;
use stdClass;
/**
* Contains information about the character set.
*
* @see https://www.php.net/manual/en/mysqli.get-charset
*/
class MariaDBCharacterSetInfo {
private stdClass $charSet;
/**
* Creates a new character set info instance.
*
* @param stdClass $charSet Anonymous object containing the information.
* @return MariaDBCharacterSetInfo Character set information class.
*/
public function __construct(stdClass $charSet) {
$this->charSet = $charSet;
}
/**
* Returns the name of the current character set.
*
* @return string Character set name.
*/
public function getCharacterSet(): string {
return $this->charSet->charset;
}
/**
* Returns the name of the default collation.
*
* @return string Default collation name.
*/
public function getDefaultCollation(): string {
return $this->charSet->collation;
}
/**
* Returns the path to the directory the charcter was read from.
* May be empty for built-in character sets.
*
* @return string Source directory.
*/
public function getDirectory(): string {
return $this->charSet->dir;
}
/**
* Returns the minimum character width in bytes for this character set.
*
* @return int Minimum character width in bytes.
*/
public function getMinimumWidth(): int {
return $this->charSet->min_length;
}
/**
* Returns the maximum character width in bytes for this character set.
*
* @return int Maximum character width in bytes.
*/
public function getMaximumWidth(): int {
return $this->charSet->max_length;
}
/**
* Returns the internal numeric identifier for this character set.
*
* @return int Character set identifier.
*/
public function getId(): int {
return $this->charSet->number;
}
/**
* Returns the character set status.
*
* Whatever that means. Given the (?) in the official documentation, not even they know.
*
* @return int Character set status.
*/
public function getState(): int {
return $this->charSet->state;
}
}

View File

@ -0,0 +1,401 @@
<?php
// MariaDBConnection.php
// Created: 2021-04-30
// Updated: 2022-02-27
namespace Index\Data\MariaDB;
use mysqli;
use mysqli_sql_exception;
use InvalidArgumentException;
use RuntimeException;
use Index\AString;
use Index\Version;
use Index\Data\BeginTransactionFailedException;
use Index\Data\DataException;
use Index\Data\IDbConnection;
use Index\Data\IDbTransactions;
use Index\Data\CommitFailedException;
use Index\Data\ConnectionFailedException;
use Index\Data\ReleaseSavePointFailedException;
use Index\Data\RollbackFailedException;
use Index\Data\SavePointFailedException;
use Index\Data\QueryExecuteException;
/**
* Represents a connection with a MariaDB or MySQL database server.
*/
class MariaDBConnection implements IDbConnection, IDbTransactions {
/**
* Refresh grant tables.
*
* @var int
*/
public const REFRESH_GRANT = MYSQLI_REFRESH_GRANT;
/**
* Flushes logs, like the FLUSH LOGS; statement.
*
* @var int
*/
public const REFRESH_LOG = MYSQLI_REFRESH_LOG;
/**
* Refresh tables, like the FLUSH TABLES; statement.
*
* @var int
*/
public const REFRESH_TABLES = MYSQLI_REFRESH_TABLES;
/**
* Refreshes the hosts cache, like the FLUSH HOSTS; statement.
*
* @var int
*/
public const REFRESH_HOSTS = MYSQLI_REFRESH_HOSTS;
/**
* Refresh the status variables, like the FLUSH STATUS; statement.
*
* @var int
*/
public const REFRESH_STATUS = MYSQLI_REFRESH_STATUS;
/**
* Flushes the thread cache.
*
* @var int
*/
public const REFRESH_THREADS = MYSQLI_REFRESH_THREADS;
/**
* Resets information about the master server and restarts on slave replication servers,
* like the RESET SLAVE; statement.
*
* @var int
*/
public const REFRESH_SLAVE = MYSQLI_REFRESH_SLAVE;
/**
* Removes binary log files listed in the binary log index and truncates the index file
* on master replication servers, like the RESET MASTER; statement.
*/
public const REFRESH_MASTER = MYSQLI_REFRESH_MASTER;
private mysqli $connection;
/**
* Creates a new instance of MariaDBConnection.
*
* @param MariaDBConnectionInfo $connectionInfo Information about the connection.
* @return MariaDBConnection A new instance of MariaDBConnection.
*/
public function __construct(MariaDBConnectionInfo $connectionInfo) {
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
// I'm not sure if calling "new mysqli" without arguments is equivalent to this
// the documentation would suggest it's not and that it just pulls from the config
// nothing suggests otherwise too.
// The output of mysqli_init is just an object anyway so we can safely use it instead
// but continue to use it as an object.
$this->connection = mysqli_init();
$this->connection->options(MYSQLI_OPT_LOCAL_INFILE, 0);
if($connectionInfo->hasCharacterSet())
$this->connection->options(MYSQLI_SET_CHARSET_NAME, $connectionInfo->getCharacterSet());
if($connectionInfo->hasInitCommand())
$this->connection->options(MYSQLI_INIT_COMMAND, $connectionInfo->getInitCommand());
$flags = $connectionInfo->shouldUseCompression() ? MYSQLI_CLIENT_COMPRESS : 0;
if($connectionInfo->isSecure()) {
$flags |= MYSQLI_CLIENT_SSL;
if($connectionInfo->shouldVerifyCertificate())
$this->connection->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
else
$flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
$this->connection->ssl_set(
$connectionInfo->getKeyPath(),
$connectionInfo->getCertificatePath(),
$connectionInfo->getCertificateAuthorityPath(),
$connectionInfo->getTrustedCertificatesPath(),
$connectionInfo->getCipherAlgorithms()
);
}
try {
if($connectionInfo->isUnixSocket())
$this->connection->real_connect(
'',
$connectionInfo->getUserName(),
$connectionInfo->getPassword(),
$connectionInfo->getDatabaseName(),
-1,
$connectionInfo->getSocketPath(),
$flags
);
else
$this->connection->real_connect(
$connectionInfo->getHost(),
$connectionInfo->getUserName(),
$connectionInfo->getPassword(),
$connectionInfo->getDatabaseName(),
$connectionInfo->getPort(),
'',
$flags
);
} catch(mysqli_sql_exception $ex) {
throw new ConnectionFailedException($ex->getMessage(), $ex->getCode(), $ex);
}
}
/**
* Gets the number of rows affected by the last operation.
*
* @return int|string Number of rows affected by the last operation.
*/
public function getAffectedRows(): int|string {
return $this->connection->affected_rows;
}
/**
* Gets the name of the currently active character set.
*
* @return string Name of the character set.
*/
public function getCharacterSet(): string {
return $this->connection->character_set_name();
}
/**
* Switch to a different character set.
*
* @param string $charSet Name of the new character set.
* @throws InvalidArgumentException Switching to new character set failed.
*/
public function setCharacterSet(string $charSet): void {
if(!$this->connection->set_charset($charSet))
throw new InvalidArgumentException('$charSet is not a supported character set.');
}
/**
* Returns info about the currently character set and collation.
*
* @return MariaDBCharacterSetInfo Information about the character set.
*/
public function getCharacterSetInfo(): MariaDBCharacterSetInfo {
return new MariaDBCharacterSetInfo($this->connection->get_charset());
}
/**
* Switches the connection to a different user and default database.
*
* @param string $userName New user name.
* @param string $password New password.
* @param string|null $database New default database.
* @throws DataException If the user switch action failed.
*/
public function switchUser(string $userName, string $password, string|null $database = null): void {
if(!$this->connection->change_user($userName, $password, $database === null ? null : $database))
throw new DataException($this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Switches the default database for this connection.
*
* @param string $database New default database.
* @throws DataException If the database switch failed.
*/
public function switchDatabase(string $database): void {
if(!$this->connection->select_db($database))
throw new DataException($this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Gets the version of the server.
*
* @return Version Version of the server.
*/
public function getServerVersion(): Version {
return MariaDBBackend::intToVersion($this->connection->server_version);
}
/**
* Gets the last error code.
*
* @return int Last error code.
*/
public function getLastErrorCode(): int {
return $this->connection->errno;
}
/**
* Gets the last error string.
*
* @return string Last error string.
*/
public function getLastErrorString(): string {
return $this->connection->error;
}
/**
* Gets the current SQL State of the connection.
*
* @return string Current SQL State.
*/
public function getSQLState(): string {
return $this->connection->sqlstate;
}
/**
* Gets a list of errors from the last command.
*
* @return array List of last errors.
*/
public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->connection->error_list);
}
/**
* Gets the number of columns for the last query.
*
* @return int
*/
public function getLastFieldCount(): int {
return $this->connection->field_count;
}
/**
* Gets list of warnings.
*
* The result of SHOW WARNINGS;
*
* @return array List of warnings.
*/
public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->connection->get_warnings());
}
/**
* Returns the number of warnings for the last query.
*
* @return int Amount of warnings.
*/
public function getWarningCount(): int {
return $this->connection->warning_count;
}
public function getLastInsertId(): int|string {
return $this->connection->insert_id;
}
/**
* Sends a keep alive request to the server, or tries to reconnect if it has gone down.
*
* @return bool true if the keep alive was successful, false if the server is Gone.
*/
public function ping(): bool {
return $this->connection->ping();
}
/**
* Runs various refresh operations, see the REFRESH_ constants for the flags.
*
* @param int $flags A bitset of REFRESH_ flags.
*/
public function refresh(int $flags): void {
$this->connection->refresh($flags);
}
public function setAutoCommit(bool $state): void {
$this->connection->autocommit($state);
}
public function beginTransaction(): void {
if(!$this->connection->begin_transaction())
throw new BeginTransactionFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function commit(): void {
if(!$this->connection->commit())
throw new CommitFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function rollback(?string $name = null): void {
if(!$this->connection->rollback(0, $name))
throw new RollbackFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function savePoint(string $name): void {
if(!$this->connection->savepoint($name))
throw new SavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function releaseSavePoint(string $name): void {
if(!$this->connection->release_savepoint($name))
throw new ReleaseSavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
/**
* Gets the thread ID for the current connection.
*
* @return int Thread ID.
*/
public function getThreadId(): int {
return $this->connection->thread_id;
}
/**
* Asks the server to kill a thread.
*
* @param int $threadId ID of the thread that should be killed.
* @return bool true if the thread was killed, false if not.
*/
public function killThread(int $threadId): bool {
return $this->connection->kill($threadId);
}
/**
* @return MariaDBStatement A database statement.
*/
public function prepare(string $query): MariaDBStatement {
$statement = $this->connection->prepare($query);
if($statement === false)
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return new MariaDBStatement($statement);
}
/**
* @return MariaDBResult A database result.
*/
public function query(string $query): MariaDBResult {
$result = $this->connection->query($query, MYSQLI_STORE_RESULT);
if($result === false)
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
// Yes, this always uses Native, for some reason the stupid limitation in libmysql only applies to preparing
return new MariaDBResultNative($result);
}
public function execute(string $query): int|string {
if(!$this->connection->real_query($query))
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return $this->connection->affected_rows;
}
/**
* Closes the connection and associated resources.
*/
public function close(): void {
$this->connection->close();
}
/**
* Closes the connection and associated resources.
*/
public function __destruct() {
$this->close();
}
}

View File

@ -0,0 +1,438 @@
<?php
// MariaDBConnectionInfo.php
// Created: 2021-04-30
// Updated: 2022-02-28
namespace Index\Data\MariaDB;
use InvalidArgumentException;
use Index\Data\IDbConnectionInfo;
use Index\Net\EndPoint;
use Index\Net\DnsEndPoint;
use Index\Net\IPAddress;
use Index\Net\IPEndPoint;
use Index\Net\UnixEndPoint;
/**
* Describes a MariaDB or MySQL connection.
*/
class MariaDBConnectionInfo implements IDbConnectionInfo {
private EndPoint $endPoint;
private string $userName;
private string $password;
private string $dbName;
private ?string $charSet;
private ?string $initCommand;
private ?string $keyPath;
private ?string $certPath;
private ?string $certAuthPath;
private ?string $trustedCertsPath;
private ?string $cipherAlgos;
private bool $verifyCert;
private bool $useCompression;
/**
* Creates an instance of MariaDBConnectionInfo.
*
* @param EndPoint $endPoint End point at which the server can be found.
* @param string $userName User name to use for the connection.
* @param string $password Password with which the user authenticates.
* @param string $dbName Default database name.
* @param string|null $charSet Default character set.
* @param string|null $initCommand Command to execute on connect.
* @param string|null $keyPath Path at which to find the private key for SSL.
* @param string|null $certPath Path at which to find the certificate file for SSL.
* @param string|null $certAuthPath Path at which to find the certificate authority file for SSL.
* @param string|null $trustedCertsPath Path at which to find the trusted SSL CA certificates for SSL in PEM format.
* @param string|null $cipherAlgos List of SSL encryption cipher that are allowed.
* @param bool $verifyCert true if the client should verify the server's SSL certificate, false if not.
* @param bool $useCompression true if compression should be used, false if not.
* @return MariaDBConnectionInfo A connection info instance representing the given information.
*/
public function __construct(
EndPoint $endPoint,
string $userName,
string $password,
string $dbName,
string|null $charSet,
string|null $initCommand,
string|null $keyPath,
string|null $certPath,
string|null $certAuthPath,
string|null $trustedCertsPath,
string|null $cipherAlgos,
bool $verifyCert,
bool $useCompression
) {
if(!($endPoint instanceof IPEndPoint)
&& !($endPoint instanceof DnsEndPoint)
&& !($endPoint instanceof UnixEndPoint))
throw new InvalidArgumentException('$endPoint must be of type IPEndPoint, DnsEndPoint or UnixEndPoint.');
$this->endPoint = $endPoint;
$this->userName = $userName;
$this->password = $password;
$this->dbName = $dbName;
$this->charSet = empty($charSet) ? null : $charSet;
$this->initCommand = empty($initCommand) ? null : $initCommand;
$this->keyPath = empty($keyPath) ? null : $keyPath;
$this->certPath = empty($certPath) ? null : $certPath;
$this->certAuthPath = empty($certAuthPath) ? null : $certAuthPath;
$this->trustedCertsPath = empty($trustedCertsPath) ? null : $trustedCertsPath;
$this->cipherAlgos = empty($cipherAlgos) ? null : $cipherAlgos;
$this->verifyCert = $verifyCert;
$this->useCompression = $useCompression;
}
/**
* Gets the end point value.
*
* @return EndPoint An instance of EndPoint.
*/
public function getEndPoint(): EndPoint {
return $this->endPoint;
}
/**
* Returns whether the end point is a unix socket path.
*
* @return bool true if the end point is a unix socket, false if not.
*/
public function isUnixSocket(): bool {
return $this->endPoint instanceof UnixEndPoint;
}
/**
* Gets the host value of the end point.
*
* Will return an empty string if the end point is a unix socket path.
*
* @return string Target host name.
*/
public function getHost(): string {
if($this->endPoint instanceof DnsEndPoint)
return $this->endPoint->getHost();
if($this->endPoint instanceof IPEndPoint)
return $this->endPoint->getAddress()->getCleanAddress();
return '';
}
/**
* Gets the port value of the end point.
*
* Will return -1 if the end point is a unix socket path.
*
* @return int Target port number.
*/
public function getPort(): int {
if(($this->endPoint instanceof DnsEndPoint) || ($this->endPoint instanceof IPEndPoint)) {
$port = $this->endPoint->getPort();
return $port < 1 ? 3306 : $port;
}
return -1;
}
/**
* Gets the unix socket path.
*
* Will return an empty string if the end point is not a unix socket path.
*
* @return string Target unix socket path.
*/
public function getSocketPath(): string {
if($this->endPoint instanceof UnixEndPoint)
return $this->endPoint->getPath();
return '';
}
/**
* Returns the connection user name.
*
* @return string Connection user name.
*/
public function getUserName(): string {
return $this->userName;
}
/**
* Returns the connection user password.
*
* @return string Connection user password.
*/
public function getPassword(): string {
return $this->password;
}
/**
* Returns the connection default database name.
*
* @return string Default database name.
*/
public function getDatabaseName(): string {
return $this->dbName;
}
/**
* Whether this connection info carries a default character set name.
*
* @return bool true if it has a character set name, false if not.
*/
public function hasCharacterSet(): bool {
return $this->charSet !== null;
}
/**
* Returns the default character set name.
*
* @return ?string Default character set name.
*/
public function getCharacterSet(): ?string {
return $this->charSet;
}
/**
* Whether this connection info carries an initialisation command.
*
* @return bool true if it has an initialisation command, false if not.
*/
public function hasInitCommand(): bool {
return $this->initCommand !== null;
}
/**
* Returns the initialisation command.
*
* @return ?string Initialisation command.
*/
public function getInitCommand(): ?string {
return $this->initCommand;
}
/**
* Whether SSL is to be used with the connection.
*
* @return bool true if SSL should be used, false if not.
*/
public function isSecure(): bool {
return $this->keyPath !== null;
}
/**
* Returns the path to the private key to use for this connection.
*
* @return ?string Private key path.
*/
public function getKeyPath(): ?string {
return $this->keyPath;
}
/**
* Returns the path to the certificate to use for this connection.
*
* @return ?string Certificate path.
*/
public function getCertificatePath(): ?string {
return $this->certPath;
}
/**
* Returns the path to the certificate authority file to use for this connection.
*
* @return ?string Certificate authority file path.
*/
public function getCertificateAuthorityPath(): ?string {
return $this->certAuthPath;
}
/**
* Returns the path to the trusted CA certificates directory for this connection.
*
* @return ?string Trusted CA certificates directory path.
*/
public function getTrustedCertificatesPath(): ?string {
return $this->trustedCertsPath;
}
/**
* Returns the list of allowed SSL cipher algorithms.
*
* @return ?string Allowed SSL cipher algorithms.
*/
public function getCipherAlgorithms(): ?string {
return $this->cipherAlgos;
}
/**
* Whether the client should verify the validity of the server's certificate.
*
* @return bool true if the certificate should be verified, false if not.
*/
public function shouldVerifyCertificate(): bool {
return $this->verifyCert;
}
/**
* Whether compression should be applied on the connection.
*
* Only makes sense for remote connections, don't use this locally...
*
* @return bool true if compression should be used, false if not.
*/
public function shouldUseCompression(): bool {
return $this->useCompression;
}
/**
* Creates a connection info instance with a hostname and port number.
*
* @param string $host Server host name.
* @param int $port Server port.
* @param string $userName User name to use for the connection.
* @param string $password Password with which the user authenticates.
* @param string $dbName Default database name.
* @param string|null $charSet Default character set.
* @param string|null $initCommand Command to execute on connect.
* @param string|null $keyPath Path at which to find the private key for SSL.
* @param string|null $certPath Path at which to find the certificate file for SSL.
* @param string|null $certAuthPath Path at which to find the certificate authority file for SSL.
* @param string|null $trustedCertsPath Path at which to find the trusted SSL CA certificates for SSL in PEM format.
* @param string|null $cipherAlgos List of SSL encryption cipher that are allowed.
* @param bool $verifyCert true if the client should verify the server's SSL certificate, false if not.
* @param bool $useCompression true if compression should be used, false if not.
* @return MariaDBConnectionInfo A connection info instance representing the given information.
*/
public static function createHost(
string $host,
int $port,
string $userName,
string $password,
string $dbName = '',
string|null $charSet = null,
string|null $initCommand = null,
string|null $keyPath = null,
string|null $certPath = null,
string|null $certAuthPath = null,
string|null $trustedCertsPath = null,
string|null $cipherAlgos = null,
bool $verifyCert = false,
bool $useCompression = false
): MariaDBConnectionInfo {
return new MariaDBConnectionInfo(
new DnsEndPoint($host, $port),
$userName,
$password,
$dbName,
$charSet,
$initCommand,
$keyPath,
$certPath,
$certAuthPath,
$trustedCertsPath,
$cipherAlgos,
$verifyCert,
$useCompression
);
}
/**
* Creates a connection info instance with a unix socket path.
*
* @param string $path Path to the unix socket.
* @param string $userName User name to use for the connection.
* @param string $password Password with which the user authenticates.
* @param string $dbName Default database name.
* @param string|null $charSet Default character set.
* @param string|null $initCommand Command to execute on connect.
* @param string|null $keyPath Path at which to find the private key for SSL.
* @param string|null $certPath Path at which to find the certificate file for SSL.
* @param string|null $certAuthPath Path at which to find the certificate authority file for SSL.
* @param string|null $trustedCertsPath Path at which to find the trusted SSL CA certificates for SSL in PEM format.
* @param string|null $cipherAlgos List of SSL encryption cipher that are allowed.
* @param bool $verifyCert true if the client should verify the server's SSL certificate, false if not.
* @param bool $useCompression true if compression should be used, false if not.
* @return MariaDBConnectionInfo A connection info instance representing the given information.
*/
public static function createUnix(
string $path,
string $userName,
string $password,
string $dbName = '',
string|null $charSet = null,
string|null $initCommand = null,
string|null $keyPath = null,
string|null $certPath = null,
string|null $certAuthPath = null,
string|null $trustedCertsPath = null,
string|null $cipherAlgos = null,
bool $verifyCert = false,
bool $useCompression = false
): MariaDBConnectionInfo {
return new MariaDBConnectionInfo(
UnixEndPoint::parse($path),
$userName,
$password,
$dbName,
$charSet,
$initCommand,
$keyPath,
$certPath,
$certAuthPath,
$trustedCertsPath,
$cipherAlgos,
$verifyCert,
$useCompression
);
}
/**
* Tries to parse an end point string and creates a connection info instance using it..
*
* Supports IPv4, IPv6 and DNS hostnames with and without a port number and Unix socket paths prefixed with unix://
*
* @param string $endPoint End point string.
* @param string $userName User name to use for the connection.
* @param string $password Password with which the user authenticates.
* @param string $dbName Default database name.
* @param string|null $charSet Default character set.
* @param string|null $initCommand Command to execute on connect.
* @param string|null $keyPath Path at which to find the private key for SSL.
* @param string|null $certPath Path at which to find the certificate file for SSL.
* @param string|null $certAuthPath Path at which to find the certificate authority file for SSL.
* @param string|null $trustedCertsPath Path at which to find the trusted SSL CA certificates for SSL in PEM format.
* @param string|null $cipherAlgos List of SSL encryption cipher that are allowed.
* @param bool $verifyCert true if the client should verify the server's SSL certificate, false if not.
* @param bool $useCompression true if compression should be used, false if not.
* @return MariaDBConnectionInfo A connection info instance representing the given information.
*/
public static function create(
string $endPoint,
string $userName,
string $password,
string $dbName = '',
string|null $charSet = null,
string|null $initCommand = null,
string|null $keyPath = null,
string|null $certPath = null,
string|null $certAuthPath = null,
string|null $trustedCertsPath = null,
string|null $cipherAlgos = null,
bool $verifyCert = false,
bool $useCompression = false
): MariaDBConnectionInfo {
return new MariaDBConnectionInfo(
EndPoint::parse($endPoint),
$userName,
$password,
$dbName,
$charSet,
$initCommand,
$keyPath,
$certPath,
$certAuthPath,
$trustedCertsPath,
$cipherAlgos,
$verifyCert,
$useCompression
);
}
}

View File

@ -0,0 +1,81 @@
<?php
// MariaDBParameter.php
// Created: 2021-05-02
// Updated: 2022-02-02
namespace Index\Data\MariaDB;
use Index\Data\DbTools;
use Index\Data\DbType;
/**
* Represents a bound parameter.
*/
class MariaDBParameter {
private int $ordinal;
private int $type;
private mixed $value;
/**
* Create a new MariaDBParameter.
*
* @param int $ordinal Parameter number.
* @param int $type DbType number.
* @param mixed $value Value to bind.
* @return MariaDBParameter A new parameter instance.
*/
public function __construct(int $ordinal, int $type, mixed $value) {
$this->ordinal = $ordinal;
if($type == DbType::AUTO)
$type = DbTools::detectType($value);
$this->type = $type;
$this->value = $type === DbType::NULL ? null : $value;
}
/**
* Gets the parameter number.
*
* @return int Parameter number.
*/
public function getOrdinal(): int {
return $this->ordinal;
}
/**
* Gets the DbType of for the parameter.
*
* @return int DbType.
*/
public function getDbType(): int {
return $this->type;
}
/**
* Gets the type character for the mysqli bind.
*
* @return string Type character.
*/
public function getBindType(): string {
switch($this->type) {
case DbType::INTEGER:
return 'i';
case DbType::FLOAT:
return 'd';
case DbType::BLOB:
return 'b';
default:
return 's';
}
}
/**
* Gets the value for the parameter.
*
* @return mixed Value.
*/
public function getValue(): mixed {
return $this->value;
}
}

View File

@ -0,0 +1,103 @@
<?php
// MariaDBResult.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\MariaDB;
use mysqli_result;
use mysqli_stmt;
use InvalidArgumentException;
use Index\AString;
use Index\WString;
use Index\Data\IDbResult;
use Index\IO\Stream;
use Index\IO\TempFileStream;
/**
* Represents a MariaDB/MySQL database result.
*/
abstract class MariaDBResult implements IDbResult {
protected mysqli_stmt|mysqli_result $result;
protected array $currentRow = [];
/**
* Creates a new MariaDBResult instance.
*
* @param mysqli_stmt|mysqli_result $result A result to work with.
* @return MariaDBResult A result instance.
*/
public function __construct(mysqli_stmt|mysqli_result $result) {
$this->result = $result;
}
/**
* Gets the number of available rows in this result.
*
* @return int|string Number of available rows.
*/
public function getRowCount(): int|string {
return $this->result->num_rows;
}
/**
* Gets the number of fields in the result per row.
*
* @return int Number of fields.
*/
public function getFieldCount(): int {
return $this->result->field_count;
}
/**
* Seek to a specific row.
*
* @param int $offset Offset of the row.
*/
public function seekRow(int $offset): void {
if(!$this->result->data_seek($offset))
throw new InvalidArgumentException('$offset is not a valid row offset.');
}
abstract public function next(): bool;
public function isNull(int|string $index): bool {
return array_key_exists($index, $this->currentRow) && $this->currentRow[$index] === null;
}
public function getValue(int|string $index): mixed {
return $this->currentRow[$index] ?? null;
}
public function getString(int|string $index): string {
return strval($this->getValue($index));
}
public function getAString(int|string $index): AString {
return new AString($this->getString($index));
}
public function getWString(int|string $index, string $encoding): WString {
return new WString($this->getString($index), $encoding);
}
public function getInteger(int|string $index): int {
return intval($this->getValue($index));
}
public function getFloat(int|string $index): float {
return floatval($this->getValue($index));
}
public function getStream(int|string $index): ?Stream {
if($this->isNull($index))
return null;
return new TempFileStream($this->getValue($index));
}
abstract function close(): void;
public function __destruct() {
$this->close();
}
}

View File

@ -0,0 +1,53 @@
<?php
// MariaDBResultLib.php
// Created: 2021-05-02
// Updated: 2021-05-04
namespace Index\Data\MariaDB;
use mysqli_stmt;
use Index\Data\QueryExecuteException;
/**
* Implementation of MariaDBResult for libmysql.
*
* @internal
*/
class MariaDBResultLib extends MariaDBResult {
private array $fields = [];
private array $data = [];
public function __construct(mysqli_stmt $statement) {
parent::__construct($statement);
if(!$statement->store_result())
throw new QueryExecuteException($statement->error, $statement->errno);
$metadata = $statement->result_metadata();
while($field = $metadata->fetch_field())
$this->fields[] = &$this->data[$field->name];
call_user_func_array([$statement, 'bind_result'], $this->fields);
}
public function next(): bool {
$result = $this->result->fetch();
if($result === false)
throw new QueryExecuteException($this->result->error, $this->result->errno);
if($result === null)
return false;
$i = 0;
$this->currentRow = [];
foreach($this->data as $key => $value) {
$this->currentRow[$i++] = $value;
$this->currentRow[$key] = $value;
}
return true;
}
public function close(): void {
$this->result->free_result();
}
}

View File

@ -0,0 +1,40 @@
<?php
// MariaDBResultNative.php
// Created: 2021-05-02
// Updated: 2021-05-04
namespace Index\Data\MariaDB;
use mysqli_result;
use mysqli_stmt;
use Index\Data\QueryExecuteException;
/**
* Implementation of MariaDBResult for mysqlnd.
*
* @internal
*/
class MariaDBResultNative extends MariaDBResult {
public function __construct(mysqli_stmt|mysqli_result $result) {
if($result instanceof mysqli_stmt) {
$_result = $result->get_result();
if($_result === false)
throw new QueryExecuteException($result->error, $result->errno);
$result = $_result;
}
parent::__construct($result);
}
public function next(): bool {
$result = $this->result->fetch_array(MYSQLI_BOTH);
if($result === null)
return false;
$this->currentRow = $result;
return true;
}
public function close(): void {
$this->result->close();
}
}

View File

@ -0,0 +1,165 @@
<?php
// MariaDBStatement.php
// Created: 2021-05-02
// Updated: 2022-02-27
namespace Index\Data\MariaDB;
use mysqli_stmt;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbType;
use Index\Data\QueryExecuteException;
use Index\Data\IDbStatement;
use Index\IO\Stream;
/**
* Represents a MariaDB/MySQL prepared statement.
*/
class MariaDBStatement implements IDbStatement {
private mysqli_stmt $statement;
private array $params = [];
/**
* Creates a MariaDBStatement.
*
* @param mysqli_stmt $statement Underlying statement object.
* @return MariaDBStatement A new statement instance.
*/
public function __construct(mysqli_stmt $statement) {
$this->statement = $statement;
}
private static bool $constructed = false;
private static string $resultImplementation;
/**
* Determines which MariaDBResult implementation should be used.
*
* @internal
*/
public static function construct(): void {
if(self::$constructed !== false)
throw new RuntimeException('Static constructor was already called.');
self::$constructed = true;
self::$resultImplementation = function_exists('mysqli_stmt_get_result')
? MariaDBResultNative::class
: MariaDBResultLib::class;
}
public function getParameterCount(): int {
return $this->statement->param_count;
}
public function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void {
if($ordinal < 1 || $ordinal > $this->getParameterCount())
throw new InvalidArgumentException('$ordinal is not a valid parameter number.');
$this->params[$ordinal - 1] = new MariaDBParameter($ordinal, $type, $value);
}
/**
* Gets the last error code.
*
* @return int Last error code.
*/
public function getLastErrorCode(): int {
return $this->statement->errno;
}
/**
* Gets the last error string.
*
* @return string Last error string.
*/
public function getLastErrorString(): string {
return $this->statement->error;
}
/**
* Gets the current SQL State of this statement.
*
* @return string Current SQL State.
*/
public function getSQLState(): string {
return $this->statement->sqlstate;
}
/**
* Gets a list of errors from the last command.
*
* @return array List of last errors.
*/
public function getLastErrors(): array {
return MariaDBWarning::fromLastErrors($this->statement->error_list);
}
/**
* Gets list of warnings.
*
* The result of SHOW WARNINGS;
*
* @return array List of warnings.
*/
public function getWarnings(): array {
return MariaDBWarning::fromGetWarnings($this->statement->get_warnings());
}
public function getResult(): MariaDBResult {
return new self::$resultImplementation($this->statement);
}
public function getLastInsertId(): int|string {
return $this->statement->insert_id;
}
public function execute(): void {
$args = [''];
foreach($this->params as $key => $param) {
$args[0] .= $param->getBindType();
$type = $param->getDbType();
$value = $param->getValue();
if($type === DbType::NULL)
$value = null;
elseif($type === DbType::BLOB) {
if($value instanceof Stream) {
while(($data = $value->read(8192)) !== null)
$this->statement->send_long_data($key, $data);
} elseif(is_resource($value)) {
while($data = fread($value, 8192))
$this->statement->send_long_data($key, $data);
} else
$this->statement->send_long_data($key, strval($value));
$value = null;
}
${"value{$key}"} = $value;
$args[] = &${"value{$key}"};
}
if(!empty($args[0]))
call_user_func_array([$this->statement, 'bind_param'], $args);
if(!$this->statement->execute())
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
}
public function reset(): void {
$this->params = [];
if(!$this->statement->reset())
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
}
public function close(): void {
$this->statement->close();
}
public function __destruct() {
$this->close();
}
}
MariaDBStatement::construct();

View File

@ -0,0 +1,95 @@
<?php
// MariaDBWarning.php
// Created: 2021-05-02
// Updated: 2022-02-27
namespace Index\Data\MariaDB;
use mysqli_warning;
use Stringable;
use Index\XArray;
/**
* Represents a MariaDB/MySQL warning.
*/
class MariaDBWarning implements Stringable {
private string $message;
private string $sqlState;
private int $code;
/**
* Creates a new MariaDBWarning object.
*
* @param string $message Warning message string.
* @param string $sqlState SQL State of the warning.
* @param int $code Error code of the warning.
* @return MariaDBWarning A new warning instance.
*/
public function __construct(string $message, string $sqlState, int $code) {
$this->message = $message;
$this->sqlState = $sqlState;
$this->code = $code;
}
/**
* Gets the warning message string.
*
* @return string Warning message string.
*/
public function getMessage(): string {
return $this->message;
}
/**
* Gets the warning SQL State.
*
* @return string SQL State.
*/
public function getSQLState(): string {
return $this->sqlState;
}
/**
* Gets the warning error code.
*
* @return int Warning error code.
*/
public function getCode(): int {
return $this->code;
}
public function __toString(): string {
return sprintf('[%s] %d: %s', $this->sqlState, $this->code, $this->message);
}
/**
* Creates an array of warning objects from the output of $last_errors.
*
* @param array $errors Array of warning arrays.
* @return array Array of warnings objects.
*/
public static function fromLastErrors(array $errors): array {
return XArray::select($errors, fn($error) => new MariaDBWarning($error['error'], $error['sqlstate'], $error['errno']));
}
/**
* Creates an array of warning objects from a mysqli_warning instance.
*
* @param mysqli_warning|false $warnings Warning object.
* @return array Array of warning objects.
*/
public static function fromGetWarnings(mysqli_warning|false $warnings): array {
$array = [];
if($warnings !== false)
do {
$array[] = new MariaDBWarning(
$warnings->message,
$warnings->sqlstate,
$warnings->errno
);
} while($warnings->next());
return $array;
}
}

View File

@ -0,0 +1,36 @@
<?php
// NullDbBackend.php
// Created: 2021-05-02
// Updated: 2022-02-28
namespace Index\Data\NullDb;
use Index\Data\IDbBackend;
use Index\Data\IDbConnection;
use Index\Data\IDbConnectionInfo;
/**
* Information about the dummy database layer.
*/
class NullDbBackend implements IDbBackend {
public function isAvailable(): bool {
return true;
}
/**
* Creates a dummy database connection.
*
* @param NullDbConnectionInfo $connectionInfo Dummy connection info.
* @return NullDbConnection Dummy connection instance.
*/
public function createConnection(IDbConnectionInfo $connectionInfo): IDbConnection {
return new NullDbConnection;
}
/**
* @return NullDbConnectionInfo Dummy connection info instance.
*/
public function parseDsn(string|array $dsn): IDbConnectionInfo {
return new NullDbConnectionInfo;
}
}

View File

@ -0,0 +1,33 @@
<?php
// NullDbConnection.php
// Created: 2021-05-02
// Updated: 2022-02-27
namespace Index\Data\NullDb;
use Index\Data\IDbConnection;
use Index\Data\IDbStatement;
use Index\Data\IDbResult;
/**
* Represents a dummy database connection.
*/
class NullDbConnection implements IDbConnection {
public function getLastInsertId(): int|string {
return 0;
}
public function prepare(string $query): IDbStatement {
return new NullDbStatement;
}
public function query(string $query): IDbResult {
return new NullDbResult;
}
public function execute(string $query): int|string {
return 0;
}
public function close(): void {}
}

View File

@ -0,0 +1,13 @@
<?php
// NullDbConnectionInfo.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\NullDb;
use Index\Data\IDbConnectionInfo;
/**
* Represents dummy connection info.
*/
class NullDbConnectionInfo implements IDbConnectionInfo {}

View File

@ -0,0 +1,54 @@
<?php
// NullDbResult.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\NullDb;
use Index\AString;
use Index\WString;
use Index\Data\IDbResult;
use Index\IO\Stream;
/**
* Represents a dummy database result.
*/
class NullDbResult implements IDbResult {
public function next(): bool {
return false;
}
public function isNull(int|string $index): bool {
return true;
}
public function getValue(int|string $index): mixed {
return null;
}
public function getString(int|string $index): string {
return '';
}
public function getAString(int|string $index): AString {
return AString::empty();
}
public function getWString(int|string $index, string $encoding): WString {
return WString::empty();
}
public function getInteger(int|string $index): int {
return 0;
}
public function getFloat(int|string $index): float {
return 0.0;
}
public function getStream(int|string $index): ?Stream {
return null;
}
public function close(): void {}
}

View File

@ -0,0 +1,35 @@
<?php
// NullDbStatement.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\NullDb;
use Index\Data\DbType;
use Index\Data\IDbResult;
use Index\Data\IDbStatement;
/**
* Represents a dummy database statement.
*/
class NullDbStatement implements IDbStatement {
public function getParameterCount(): int {
return 0;
}
public function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void {}
public function getResult(): IDbResult {
return new NullDbResult;
}
public function getLastInsertId(): int|string {
return 0;
}
public function execute(): void {}
public function reset(): void {}
public function close(): void {}
}

View File

@ -0,0 +1,11 @@
<?php
// QueryExecuteException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when query execution fails.
*/
class QueryExecuteException extends DataException {}

View File

@ -0,0 +1,11 @@
<?php
// ReleaseSavePointFailedException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when save point release fails.
*/
class ReleaseSavePointFailedException extends TransactionException {}

View File

@ -0,0 +1,11 @@
<?php
// RollbackFailedException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when transaction rollback fails.
*/
class RollbackFailedException extends TransactionException {}

View File

@ -0,0 +1,74 @@
<?php
// SQLiteBackend.php
// Created: 2021-05-02
// Updated: 2022-02-28
namespace Index\Data\SQLite;
use SQLite3;
use InvalidArgumentException;
use Index\Version;
use Index\Data\IDbBackend;
use Index\Data\IDbConnection;
use Index\Data\IDbConnectionInfo;
/**
* Information about the SQLite 3 database layer.
*/
class SQLiteBackend implements IDbBackend {
public function isAvailable(): bool {
return extension_loaded('sqlite3');
}
/**
* Gets the version of the underlying library.
*
* @return Version Version of the library.
*/
public function getVersion(): Version {
return Version::parse(SQLite3::version()['versionString']);
}
/**
* Creates a new SQLite client.
*
* @param SQLiteConnectionInfo $connectionInfo Object that describes the desired connection.
* @return SQLiteConnection A client for an SQLite database.
*/
public function createConnection(IDbConnectionInfo $connectionInfo): IDbConnection {
if(!($connectionInfo instanceof SQLiteConnectionInfo))
throw new InvalidArgumentException('$connectionInfo must by of type SQLiteConnectionInfo');
return new SQLiteConnection($connectionInfo);
}
/**
* @return SQLiteConnectionInfo SQLite connection info.
*/
public function parseDsn(string|array $dsn): IDbConnectionInfo {
if(is_string($dsn)) {
$dsn = parse_url($dsn);
if($dsn === false)
throw new InvalidArgumentException('$dsn is not a valid uri.');
}
$path = $dsn['path'] ?? '';
if($path === 'memory:')
$path = ':memory:';
if(!isset($dsn['query'])) {
$encKey = '';
$readOnly = false;
$create = true;
} else {
parse_str(str_replace('+', '%2B', $dsn['query']), $query);
$encKey = $query['key'] ?? '';
$readOnly = !empty($query['readOnly']);
$create = empty($query['openOnly']);
}
return new SQLiteConnectionInfo($path, $encKey, $readOnly, $create);
}
}

View File

@ -0,0 +1,569 @@
<?php
// SQLiteConnection.php
// Created: 2021-05-02
// Updated: 2022-02-27
namespace Index\Data\SQLite;
use SQLite3;
use Index\Data\BeginTransactionFailedException;
use Index\Data\DataException;
use Index\Data\IDbConnection;
use Index\Data\IDbTransactions;
use Index\Data\IDbStatement;
use Index\Data\IDbResult;
use Index\Data\CommitFailedException;
use Index\Data\ReleaseSavePointFailedException;
use Index\Data\RollbackFailedException;
use Index\Data\SavePointFailedException;
use Index\Data\QueryExecuteException;
use Index\IO\GenericStream;
use Index\IO\Stream;
/**
* Represents a client for an SQLite database.
*/
class SQLiteConnection implements IDbConnection, IDbTransactions {
/**
* CREATE INDEX authorizer.
*
* Second parameter will be the name of the index.
* Third parameter will be the name of the table.
*
* @var int
*/
public const CREATE_INDEX = SQLite3::CREATE_INDEX;
/**
* CREATE TABLE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const CREATE_TABLE = SQLite3::CREATE_TABLE;
/**
* CREATE TEMP INDEX authorizer.
*
* Second parameter will be the name of the index.
* Third parameter will be the name of the table.
*
* @var int
*/
public const CREATE_TEMP_INDEX = SQLite3::CREATE_TEMP_INDEX;
/**
* CREATE TEMP TABLE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const CREATE_TEMP_TABLE = SQLite3::CREATE_TEMP_TABLE;
/**
* CREATE TEMP TRIGGER authorizer.
*
* Second parameter will be the name of the trigger.
* Third parameter will be the name of the table.
*
* @var int
*/
public const CREATE_TEMP_TRIGGER = SQLite3::CREATE_TEMP_TRIGGER;
/**
* CREATE TEMP VIEW authorizer.
*
* Second parameter will be the name of the view.
*
* @var int
*/
public const CREATE_TEMP_VIEW = SQLite3::CREATE_TEMP_VIEW;
/**
* CREATE TRIGGER authorizer.
*
* Second parameter will be the name of the trigger.
* Third parameter will be the name of the table.
*
* @var int
*/
public const CREATE_TRIGGER = SQLite3::CREATE_TRIGGER;
/**
* CREATE VIEW authorizer.
*
* Second parameter will be the name of the view.
*
* @var int
*/
public const CREATE_VIEW = SQLite3::CREATE_VIEW;
/**
* DELETE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const DELETE = SQLite3::DELETE;
/**
* DROP INDEX authorizer.
*
* Second parameter will be the name of the index.
* Third parameter will be the name of the table.
*
* @var int
*/
public const DROP_INDEX = SQLite3::DROP_INDEX;
/**
* DROP TABLE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const DROP_TABLE = SQLite3::DROP_TABLE;
/**
* DROP TEMP INDEX authorizer.
*
* Second parameter will be the name of the index.
* Third parameter will be the name of the table.
*
* @var int
*/
public const DROP_TEMP_INDEX = SQLite3::DROP_TEMP_INDEX;
/**
* DROP TEMP TABLE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const DROP_TEMP_TABLE = SQLite3::DROP_TEMP_TABLE;
/**
* DROP TEMP TRIGGER authorizer.
*
* Second parameter will be the name of the trigger.
* Third parameter will be the name of the table.
*
* @var int
*/
public const DROP_TEMP_TRIGGER = SQLite3::DROP_TEMP_TRIGGER;
/**
* DROP TEMP VIEW authorizer.
*
* Second parameter will be the name of the view.
*
* @var int
*/
public const DROP_TEMP_VIEW = SQLite3::DROP_TEMP_VIEW;
/**
* DROP TRIGGER authorizer.
*
* Second parameter will be the name of the trigger.
* Third parameter will be the name of the table.
*
* @var int
*/
public const DROP_TRIGGER = SQLite3::DROP_TRIGGER;
/**
* DROP VIEW authorizer.
*
* Second parameter will be the name of the view.
*
* @var int
*/
public const DROP_VIEW = SQLite3::DROP_VIEW;
/**
* INSERT authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const INSERT = SQLite3::INSERT;
/**
* PRAGMA authorizer.
*
* Second parameter will be the name of the pragma.
* Third parameter may be the first argument passed to the pragma.
*
* @var int
*/
public const PRAGMA = SQLite3::PRAGMA;
/**
* READ authorizer.
*
* Second parameter will be the name of the table.
* Third parameter will be the name of the column.
*
* @var int
*/
public const READ = SQLite3::READ;
/**
* SELECT authorizer.
*
* @var int
*/
public const SELECT = SQLite3::SELECT;
/**
* TRANSACTION authorizer.
*
* Second parameter will be the operation.
*
* @var int
*/
public const TRANSACTION = SQLite3::TRANSACTION;
/**
* UPDATE authorizer.
*
* Second parameter will be the name of the table.
* Third parameter will be the name of the column.
*
* @var int
*/
public const UPDATE = SQLite3::UPDATE;
/**
* ATTACH authorizer.
*
* Second parameter will be the filename.
*
* @var int
*/
public const ATTACH = SQLite3::ATTACH;
/**
* DETACH authorizer.
*
* Second parameter will be the name of the database.
*
* @var int
*/
public const DETACH = SQLite3::DETACH;
/**
* ALTER TABLE authorizer.
*
* Second parameter will be the name of the database.
* Third parameter will be the name of the table.
*
* @var int
*/
public const ALTER_TABLE = SQLite3::ALTER_TABLE;
/**
* REINDEX authorizer.
*
* Second parameter will be the name of the index.
*
* @var int
*/
public const REINDEX = SQLite3::REINDEX;
/**
* ANALYZE authorizer.
*
* Second parameter will be the name of the table.
*
* @var int
*/
public const ANALYZE = SQLite3::ANALYZE;
/**
* CREATE VTABLE authorizer.
*
* Second parameter will be the name of the table.
* Third parameter will be the name of the module.
*
* @var int
*/
public const CREATE_VTABLE = SQLite3::CREATE_VTABLE;
/**
* DROP VTABLE authorizer.
*
* Second parameter will be the name of the table.
* Third parameter will be the name of the module.
*
* @var int
*/
public const DROP_VTABLE = SQLite3::DROP_VTABLE;
/**
* FUNCTION authorizer.
*
* Third parameter will be the name of the function.
*
* @var int
*/
public const FUNCTION = SQLite3::FUNCTION;
/**
* SAVEPOINT authorizer.
*
* Second parameter will be the operation.
* Third parameter will be the name of the savepoint.
*
* @var int
*/
public const SAVEPOINT = SQLite3::SAVEPOINT;
/**
* RECURSIVE authorizer.
*
* @var int
*/
public const RECURSIVE = SQLite3::RECURSIVE;
/**
* DETERMINISTIC flag for SQLite registered functions.
*
* When specified the function will always return the same result given the same inputs within a single SQL statement.
*
* @var int
*/
public const DETERMINISTIC = SQLITE3_DETERMINISTIC;
/**
* OK authorization status.
*
* @var int
*/
public const OK = SQLite3::OK;
/**
* DENY authorization status.
*
* @var int
*/
public const DENY = SQLite3::DENY;
/**
* IGNORE authorization status.
*
* @var int
*/
public const IGNORE = SQLite3::IGNORE;
private SQLite3 $connection;
private bool $autoCommit = true;
/**
* Creates a new SQLite client.
*
* @param SQLiteConnectionInfo $connectionInfo Information about the client.
* @return SQLiteConnection A new SQLite client.
*/
public function __construct(SQLiteConnectionInfo $connectionInfo) {
$flags = 0;
if($connectionInfo->shouldReadOnly())
$flags |= SQLITE3_OPEN_READONLY;
else
$flags |= SQLITE3_OPEN_READWRITE;
if($connectionInfo->shouldCreate())
$flags |= SQLITE3_OPEN_CREATE;
$this->connection = new SQLite3(
$connectionInfo->getFileName(),
$flags,
$connectionInfo->getEncryptionKey()
);
$this->connection->enableExceptions(true);
}
/**
* Gets the number of rows affected by the last operation.
*
* @return int|string Number of rows affected by the last operation.
*/
public function getAffectedRows(): int|string {
return $this->connection->changes();
}
/**
* Registers a PHP function for use as an SQL aggregate function.
*
* @see https://www.php.net/manual/en/sqlite3.createaggregate.php
* @param string $name Name of the SQL aggregate.
* @param callable $stepCallback Callback function to be called for each row of the result set.
* Definition: step(mixed $context, int $rowNumber, mixed $value, mixed ...$values): mixed
* @param callable $finalCallback Callback function to be called to finalize the aggregate operation and return the value.
* Definition: fini(mixed $content, int $rowNumber): mixed
* @param int $argCount Number of arguments this aggregate function takes, -1 for any amount.
* @return bool true if the aggregate function was created successfully, false if not.
*/
public function createAggregate(string $name, callable $stepCallback, callable $finalCallback, int $argCount = -1): bool {
return $this->connection->createAggregate($name, $stepCallback, $finalCallback, $argCount);
}
/**
* Registers a PHP function for use as an SQL collating function.
*
* @see https://www.php.net/manual/en/sqlite3.createcollation.php
* @param string $name Name of the SQL collating function.
* @param callable $callback Callback function that returns -1, 0 or 1 to indicate sort order.
* Definition: collation(mixed $value1, mixed $value2): int
* @return bool true if the collating function was registered, false if not.
*/
public function createCollation(string $name, callable $callback): bool {
return $this->connection->createCollation($name, $callback);
}
/**
* Register a PHP function for use as an SQL scalar function.
*
* @see https://www.php.net/manual/en/sqlite3.createfunction.php
* @param string $name Name of the SQL function.
* @param callable $callback Callback function to register.
* Definition: callback(mixed $value, mixed ...$values): mixed
* @param int $argCount Number of arguments this aggregate function takes, -1 for any amount.
* @param int $flags A bitset of flags.
*/
public function createFunction(string $name, callable $callback, int $argCount = -1, int $flags = 0): bool {
return $this->connection->createFunction($name, $callback, $argCount, $flags);
}
/**
* Sets a callback to be used as an authorizer to limit what statements can do.
*
* @param callable|null $callback An authorizer callback or null to remove the previous authorizer.
* Definition: authorizer(int $actionCode, mixed $param2, mixed $param3, string $database, mixed $trigger): int
* $param2 and $param3 differ based on $actionCode.
* Must return one of the OK, DENY or IGNORE constants.
* @return bool true if the action was successful, false if not.
*/
public function setAuthorizer(callable|null $callback): bool {
return $this->connection->setAuthorizer($callback);
}
/**
* Gets the last error string.
*
* @return string Last error string.
*/
public function getLastErrorString(): string {
return $this->connection->lastErrorMsg();
}
/**
* Gets the last error code.
*
* @return int Last error code.
*/
public function getLastErrorCode(): int {
return $this->connection->lastErrorCode();
}
/**
* Opens a BLOB field as a Stream.
*
* To be used instead of getStream on SQLiteResult.
*
* @param string $table Name of the source table.
* @param string $column Name of the source column.
* @param int $rowId ID of the source row.
* @throws DataException If opening the BLOB failed.
* @return Stream BLOB field as a Stream.
*/
public function getBlobStream(string $table, string $column, int $rowId): Stream {
$handle = $this->connection->openBlob($table, $column, $rowId);
if(!is_resource($handle))
throw new DataException((string)$this->getLastErrorString(), $this->getLastErrorCode());
return new GenericStream($this->connection->openBlob($table, $column, $rowId));
}
public function getLastInsertId(): int|string {
return $this->connection->lastInsertRowID();
}
public function prepare(string $query): IDbStatement {
$statement = $this->connection->prepare($query);
if($statement === false)
throw new QueryExecuteException((string)$this->getLastErrorString(), $this->getLastErrorCode());
return new SQLiteStatement($this, $statement);
}
public function query(string $query): IDbResult {
$result = $this->connection->query($query);
if($result === false)
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return new SQLiteResult($result);
}
public function execute(string $query): int|string {
if(!$this->connection->exec($query))
throw new QueryExecuteException($this->getLastErrorString(), $this->getLastErrorCode());
return $this->connection->changes();
}
public function setAutoCommit(bool $state): void {
// there's not really a completely reliable way to set a persistent auto commit disable state
if($state === $this->autoCommit)
return;
$this->autoCommit = $state;
if($state) {
$this->commit();
} else {
$this->beginTransaction();
}
}
public function beginTransaction(): void {
if(!$this->connection->exec('BEGIN;'))
throw new BeginTransactionFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function commit(): void {
if(!$this->connection->exec('COMMIT;'))
throw new CommitFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
if(!$this->autoCommit)
$this->beginTransaction();
}
public function rollback(?string $name = null): void {
if(!$this->connection->exec(empty($name) ? 'ROLLBACK;' : "ROLLBACK TO {$name};"))
throw new CommitFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
if(!$this->autoCommit)
$this->beginTransaction();
}
public function savePoint(string $name): void {
if(!$this->connection->exec("SAVEPOINT {$name};"))
throw new SavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function releaseSavePoint(string $name): void {
if(!$this->connection->exec("RELEASE SAVEPOINT {$name};"))
throw new ReleaseSavePointFailedException((string)$this->getLastErrorString(), $this->getLastErrorCode());
}
public function close(): void {
$this->connection->close();
}
/**
* Closes the connection and associated resources.
*/
public function __destruct() {
$this->close();
}
}

View File

@ -0,0 +1,116 @@
<?php
// SQLiteConnectionInfo.php
// Created: 2021-05-02
// Updated: 2022-02-28
namespace Index\Data\SQLite;
use Index\Data\IDbConnectionInfo;
/**
* Represents information about a SQLite Client.
*/
class SQLiteConnectionInfo implements IDbConnectionInfo {
private string $fileName;
private bool $readOnly;
private bool $create;
private string $encryptionKey;
/**
* Creates a new SQLiteConnectionInfo instance.
*
* @param string $fileName Path to the SQLite database.
* @param string $encryptionKey Key to encrypt the database with.
* @param bool $readOnly Set to true if the database should be opened read-only.
* @param bool $create Set to true to create the database if it does not exist.
* @return SQLiteConnectionInfo Information to create an SQLite database client.
*/
public function __construct(
string $fileName,
string $encryptionKey,
bool $readOnly,
bool $create
) {
$this->fileName = $fileName;
$this->encryptionKey = $encryptionKey;
$this->readOnly = $readOnly;
$this->create = $create;
}
/**
* Gets the path of the SQLite database.
*/
public function getFileName(): string {
return $this->fileName;
}
/**
* Returns the key to use for encryption of the database.
*
* @return string Encryption key.
*/
public function getEncryptionKey(): string {
return $this->encryptionKey;
}
/**
* Returns whether the SQLite database should be opened read-only.
*
* @return bool true if the database should be opened read-only, false if not.
*/
public function shouldReadOnly(): bool {
return $this->readOnly;
}
/**
* Returns whether the SQLite database should be created if it does not exist.
*
* @return bool true if the database should be created, false if not.
*/
public function shouldCreate(): bool {
return $this->create;
}
/**
* Creates a connection info instance with a path.
*
* @param string $path Path to the SQLite database.
* @param string $encryptionKey Key to encrypt the database with.
* @param bool $readOnly Set to true if the database should be opened read-only.
* @param bool $create Set to true to create the database if it does not exist.
* @return SQLiteConnectionInfo Information to create an SQLite database client.
*/
public static function createPath(
string $path,
string $encryptionKey = '',
bool $readOnly = false,
bool $create = true
): SQLiteConnectionInfo {
return new SQLiteConnectionInfo(
$path,
$encryptionKey,
$readOnly,
$create
);
}
/**
* Creates a connection info instance for an in-memory database.
*
* @param string $encryptionKey Key to encrypt the database with.
* @return SQLiteConnectionInfo Information to create an SQLite database client.
*/
public static function createMemory(string $encryptionKey = ''): SQLiteConnectionInfo {
return new SQLiteConnectionInfo(':memory:', $encryptionKey, false, true);
}
/**
* Creates a connection info instance for a database stored in a temporary file.
*
* @param string $encryptionKey Key to encrypt the database with.
* @return SQLiteConnectionInfo Information to create an SQLite database client.
*/
public static function createTemp(string $encryptionKey = ''): SQLiteConnectionInfo {
return new SQLiteConnectionInfo('', $encryptionKey, false, true);
}
}

View File

@ -0,0 +1,89 @@
<?php
// SQLiteResult.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\SQLite;
use SQLite3Result;
use InvalidArgumentException;
use Index\AString;
use Index\WString;
use Index\Data\IDbResult;
use Index\IO\Stream;
use Index\IO\TempFileStream;
/**
* Represents an SQLite result set.
*/
class SQLiteResult implements IDbResult {
private SQLite3Result $result;
private array $currentRow = [];
/**
* Creates a new instance of SQLiteResult.
*
* @param SQLite3Result $result Raw underlying result class.
* @return SQLiteResult A new SQLiteResult instance.
*/
public function __construct(SQLite3Result $result) {
$this->result = $result;
}
/**
* Gets the number of columns per row in the result.
*
* @return int|string Number of columns in a row.
*/
public function getFieldCount(): int|string {
return $this->result->numColumns();
}
public function next(): bool {
$result = $this->result->fetchArray(SQLITE3_BOTH);
if($result === false)
return false;
$this->currentRow = $result;
return true;
}
public function isNull(int|string $index): bool {
return array_key_exists($index, $this->currentRow) && $this->currentRow[$index] === null;
}
public function getValue(int|string $index): mixed {
return $this->currentRow[$index] ?? null;
}
public function getString(int|string $index): string {
return strval($this->getValue($index));
}
public function getAString(int|string $index): AString {
return new AString($this->getString($index));
}
public function getWString(int|string $index, string $encoding): WString {
return new WString($this->getString($index), $encoding);
}
public function getInteger(int|string $index): int {
return intval($this->getValue($index));
}
public function getFloat(int|string $index): float {
return floatval($this->getValue($index));
}
/**
* Gets the value from the target index as a Stream.
* If you're aware that you're using SQLite it may make more sense to use SQLiteConnection::getBlobStream instead.
*/
public function getStream(int|string $index): ?Stream {
if($this->isNull($index))
return null;
return new TempFileStream($this->getValue($index));
}
public function close(): void {}
}

View File

@ -0,0 +1,100 @@
<?php
// SQLiteStatement.php
// Created: 2021-05-02
// Updated: 2022-02-16
namespace Index\Data\SQLite;
use SQLite3Result;
use SQLite3Stmt;
use InvalidArgumentException;
use RuntimeException;
use Index\Data\DbTools;
use Index\Data\DbType;
use Index\Data\QueryExecuteException;
use Index\Data\IDbStatement;
use Index\Data\IDbResult;
use Index\IO\Stream;
/**
* Represents a prepared SQLite SQL statement.
*/
class SQLiteStatement implements IDbStatement {
private SQLiteConnection $connection;
private SQLite3Stmt $statement;
private ?SQLite3Result $result = null;
/**
* Creates a new SQLiteStatement instance.
*
* @param SQLiteConnection $connection A reference to the connection which creates this statement.
* @param SQLite3Stmt $statement The raw statement instance.
* @return SQLiteStatement A new statement instance.
*/
public function __construct(SQLiteConnection $connection, SQLite3Stmt $statement) {
$this->connection = $connection;
$this->statement = $statement;
}
public function getParameterCount(): int {
return $this->statement->paramCount();
}
public function addParameter(int $ordinal, mixed $value, int $type = DbType::AUTO): void {
if($ordinal < 1 || $ordinal > $this->getParameterCount())
throw new InvalidArgumentException('$ordinal is not a valid parameter number.');
if($type === DbType::AUTO)
$type = DbTools::detectType($value);
switch($type) {
case DbType::NULL:
$value = null;
$type = SQLITE3_NULL;
break;
case DbType::INTEGER:
$type = SQLITE3_INTEGER;
break;
case DbType::FLOAT:
$type = SQLITE3_FLOAT;
break;
case DbType::STRING:
$type = SQLITE3_TEXT;
break;
case DbType::BLOB:
$type = SQLITE3_BLOB;
break;
default:
throw new InvalidArgumentException('$type is not a supported type.');
}
if(!$this->statement->bindValue($ordinal, $value, $type))
throw new QueryExecuteException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode());
}
public function getResult(): IDbResult {
if($this->result === null)
throw new RuntimeException('No result is available.');
return new SQLiteResult($this->result);
}
public function getLastInsertId(): int|string {
return $this->connection->getLastInsertId();
}
public function execute(): void {
$result = $this->statement->execute();
if($result === false)
throw new QueryExecuteException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode());
$this->result = $result;
}
public function reset(): void {
$this->result = null;
if(!$this->statement->clear())
throw new QueryExecuteException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode());
if(!$this->statement->reset())
throw new QueryExecuteException((string)$this->connection->getLastErrorString(), $this->connection->getLastErrorCode());
}
public function close(): void {}
}

View File

@ -0,0 +1,11 @@
<?php
// SavePointFailedException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception to be thrown when save point creation fails.
*/
class SavePointFailedException extends TransactionException {}

View File

@ -0,0 +1,11 @@
<?php
// TransactionException.php
// Created: 2021-05-02
// Updated: 2021-05-12
namespace Index\Data;
/**
* Exception for errors during transactions.
*/
class TransactionException extends DataException {}

667
src/DateTime.php Normal file
View File

@ -0,0 +1,667 @@
<?php
// DateTime.php
// Created: 2021-06-11
// Updated: 2022-02-27
namespace Index;
use DateInterval;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use InvalidArgumentException;
use JsonSerializable;
use RuntimeException;
use Stringable;
/**
* Represents a date and time
*/
class DateTime extends DateTimeImmutable implements JsonSerializable, Stringable, IComparable, IEquatable {
/**
* Represents Sunday for getDayOfWeek.
*
* @var int
*/
public const SUNDAY = 0;
/**
* Represents Monday for getDayOfWeek.
*
* @var int
*/
public const MONDAY = 1;
/**
* Represents Tuesday for getDayOfWeek.
*
* @var int
*/
public const TUESDAY = 2;
/**
* Represents Wednesday for getDayOfWeek.
*
* @var int
*/
public const WEDNESDAY = 3;
/**
* Represents Thursday for getDayOfWeek.
*
* @var int
*/
public const THURSDAY = 4;
/**
* Represents Friday for getDayOfWeek.
*
* @var int
*/
public const FRIDAY = 5;
/**
* Represents Saturday for getDayOfWeek.
*
* @var int
*/
public const SATURDAY = 6;
private const COMPARE = 'YmdHisu';
/**
* Creates a new DateTime object.
*
* @see https://www.php.net/manual/en/datetime.formats.php
* @param string $dateTime A date/time string.
* @param DateTimeZone|null $timeZone An object representing the desired time zone.
* @return DateTime A new DateTime instance.
*/
public function __construct(string $dateTime = 'now', DateTimeZone|null $timeZone = null) {
parent::__construct($dateTime, $timeZone);
}
/**
* Gets the date component of this instance.
*
* @return DateTime New instance with the same date but the time set to 00:00:00.
*/
public function getDate(): DateTime {
return self::create($this->getYear(), $this->getMonth(), $this->getDay(), 0, 0, 0, 0, $this->getTimezone());
}
/**
* Gets the year component of this instance.
*
* @return int Year component.
*/
public function getYear(): int {
return (int)$this->format('Y');
}
/**
* Gets the month component of this instance.
*
* @return int Month component, ranging from 1 to 12.
*/
public function getMonth(): int {
return (int)$this->format('n');
}
/**
* Gets the day component of this instance.
*
* @return int Day component, ranging from 1 to 31.
*/
public function getDay(): int {
return (int)$this->format('j');
}
/**
* Gets the day of the week represented by this instance.
*
* See the SUNDAY, MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY and SATURDAY constants for the possible return values of this function.
*
* @return int Integer between 0 and 6 representing the day of the week.
*/
public function getDayOfWeek(): int {
return (int)$this->format('w');
}
/**
* Gets the day of the year represented by this instance.
*
* @return int Day of the year, ranging from 1-366.
*/
public function getDayOfYear(): int {
return ((int)$this->format('z')) + 1;
}
/**
* Gets the ISO8601 week number of this instance.
*
* @return int Integer representing the current week number.
*/
public function getWeek(): int {
return (int)$this->format('W');
}
/**
* Gets the time of day for this instance.
*
* @return TimeSpan A TimeSpan representing the amount of time elapsed since midnight.
*/
public function getTimeOfDay(): TimeSpan {
return TimeSpan::create(0, $this->getHour(), $this->getMinute(), $this->getSecond(), $this->getMicrosecond());
}
/**
* Gets the hour component of this instance.
*
* @return int Hour component, ranging from 0 to 23.
*/
public function getHour(): int {
return (int)$this->format('G');
}
/**
* Gets the minute component of this instance.
*
* @return int Minute component, ranging from 0 to 59.
*/
public function getMinute(): int {
return (int)$this->format('i');
}
/**
* Gets the second component of this instance.
*
* @return int Second component, ranging from 0 to 59.
*/
public function getSecond(): int {
return (int)$this->format('s');
}
/**
* Gets the millisecond component of this instance.
*
* @return int Millisecond component, ranging from 0 to 999.
*/
public function getMillisecond(): int {
return (int)$this->format('v');
}
/**
* Gets the microsecond component of this instance.
*
* @return int Microsecond component, ranging from 0 to 999999.
*/
public function getMicrosecond(): int {
return (int)$this->format('u');
}
/**
* Gets the number of seconds that have elapsed since midnight January 1st, 1970 UTC for this instance.
*
* @return int Number of seconds.
*/
public function getUnixTimeSeconds(): int {
return (int)$this->format('U');
}
/**
* Gets the number of milliseconds that have elapsed since midnight January 1st, 1970 UTC for this instance.
*
* @return float Number of milliseconds.
*/
public function getUnixTimeMilliseconds(): float {
return (float)$this->format('Uv');
}
/**
* Gets whether Daylight Savings is in effect during the time this instance represents.
*
* @return bool true if DST is in effect, false if not.
*/
public function isDaylightSavingTime(): bool {
return $this->format('I') !== '0';
}
/**
* Gets whether the year this instance represents is a leap year.
*
* @return bool true if the year is a leap year, false if not.
*/
public function isLeapYear(): bool {
return $this->format('L') !== '0';
}
/**
* Gets whether this DateTime uses UTC date/time.
*
* @return bool true if UTC, false if not.
*/
public function isUTC(): bool {
return $this->getTimezone()->equals(TimeZoneInfo::utc());
}
/**
* Gets the time zone for this instance.
*
* @return TimeZoneInfo Time zone for this instance.
*/
public function getTimezone(): TimeZoneInfo {
return TimeZoneInfo::cast(parent::getTimezone());
}
/**
* Adds a period of time to this date time object.
*
* @param DateInterval $timeSpan Time period to add to this DateTime.
* @return DateTime A DateTime instance which is the sum of this instance and the provided period.
*/
public function add(DateInterval $timeSpan): DateTime {
return self::cast(parent::add($timeSpan));
}
/**
* Adds a number of years to this instance.
*
* @param float $years Number of years to add.
* @return DateTime New DateTime instance with the years added.
*/
public function addYears(float $years): DateTime {
return $this->add(TimeSpan::fromDays(365 * $years));
}
/**
* Adds a number of months to this instance.
*
* @param float $months Number of months to add.
* @return DateTime New DateTime instance with the months added.
*/
public function addMonths(float $months): DateTime {
return $this->add(TimeSpan::fromDays(365 * 31 * $months));
}
/**
* Adds a number of days to this instance.
*
* @param float $days Number of days to add.
* @return DateTime New DateTime instance with the days added.
*/
public function addDays(float $days): DateTime {
return $this->add(TimeSpan::fromDays($days));
}
/**
* Adds a number of hours to this instance.
*
* @param float $hours Number of hours to add.
* @return DateTime New DateTime instance with the hours added.
*/
public function addHours(float $hours): DateTime {
return $this->add(TimeSpan::fromHours($hours));
}
/**
* Adds a number of minutes to this instance.
*
* @param float $minutes Number of minutes to add.
* @return DateTime New DateTime instance with the minutes added.
*/
public function addMinutes(float $minutes): DateTime {
return $this->add(TimeSpan::fromMinutes($minutes));
}
/**
* Adds a number of seconds to this instance.
*
* @param float $seconds Number of seconds to add.
* @return DateTime New DateTime instance with the seconds added.
*/
public function addSeconds(float $seconds): DateTime {
return $this->add(TimeSpan::fromSeconds($seconds));
}
/**
* Adds a number of milliseconds to this instance.
*
* @param float $millis Number of milliseconds to add.
* @return DateTime New DateTime instance with the milliseconds added.
*/
public function addMilliseconds(float $millis): DateTime {
return $this->add(TimeSpan::fromMilliseconds($millis));
}
/**
* Adds a number of microseconds to this instance.
*
* @param float $micros Number of microseconds to add.
* @return DateTime New DateTime instance with the microseconds added.
*/
public function addMicroseconds(float $micros): DateTime {
return $this->add(TimeSpan::fromMicroseconds($micros));
}
/**
* Alias for subtract, must exist because of the DateTimeImmutable inheritance but I don't like short names.
*
* @internal
*/
public function sub(DateInterval $interval): DateTime {
return $this->subtract($interval);
}
/**
* Subtracts a time period from this DateTime.
*
* @param DateInterval $timeSpan Time period to be subtracted.
* @return DateTime A new DateTime instance which is this instance minus the given time period.
*/
public function subtract(DateInterval $timeSpan): DateTime {
return self::cast(parent::sub($timeSpan));
}
/**
* Alias for difference, must exist because of the DateTimeImmutable inheritance but I don't like short names.
*
* @internal
*/
public function diff(DateTimeInterface $dateTime, bool $absolute = false): TimeSpan {
return $this->difference($dateTime, $absolute);
}
/**
* Subtracts another Date/Time from this DateTime.
*
* @param DateTimeInterface $dateTime Date/time to subtract.
* @return TimeSpan Difference between this DateTime and the provided one.
*/
public function difference(DateTimeInterface $dateTime, bool $absolute = false): TimeSpan {
return TimeSpan::cast(parent::diff($dateTime, $absolute));
}
/**
* Applies a modifier on this instance and returns the result as a new instance.
*
* @see https://www.php.net/manual/en/datetime.formats.php
* @param string $modifier A date/time format.
* @return DateTime The modified date/time.
*/
public function modify(string $modifier): DateTime {
return self::cast(parent::modify($modifier));
}
/**
* Creates a new DateTime with the given date component.
*
* @param int $year Desired year component.
* @param int $month Desired month component.
* @param int $day Desired day component.
* @return DateTime A new DateTime instance with the given date component.
*/
public function setDate(int $year, int $month, int $day): DateTime {
return self::cast(parent::setDate($year, $month, $day));
}
/**
* Creates a new DateTime with the given ISO date for the date component.
*
* @param int $year Year of the date.
* @param int $week Week of the date.
* @param int $dayOfWeek Offset from the first of the week.
* @return DateTime A new DateTime instance with the given date component.
*/
public function setISODate(int $year, int $week, int $dayOfWeek = 1): DateTime {
return self::cast(parent::setISODate($year, $week, $dayOfWeek));
}
/**
* Creates a new DateTime with the given time component.
*
* @param int $hour Desired hour component.
* @param int $minute Desired minute component.
* @param int $second Desired second component.
* @param int $microsecond Desired microsecond component.
* @return DateTime A new DateTime instance with the given time component.
*/
public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0): DateTime {
return self::cast(parent::setTime($hour, $minute, $second, $microsecond));
}
/**
* Creates a new DateTime with the date and time components based on a Unix timestamp.
*
* @param int $timestamp Unix timestamp representing the date.
* @return DateTime A new DateTime instance with the given date and time.
*/
public function setTimestamp(int $timestamp): DateTime {
return self::cast(parent::setTimestamp($timestamp));
}
/**
* Creates a new DateTime with the given time zone.
*
* @param DateTimeZone $timeZone An object representing the desired time zone.
* @return DateTime A new DateTime with the given time zone.
*/
public function setTimezone(DateTimeZone $timeZone): DateTime {
return self::cast(parent::setTimezone($timeZone));
}
public function compare(mixed $other): int {
if($other instanceof DateTimeInterface)
return strcmp($this->format(self::COMPARE), $other->format(self::COMPARE));
return -1;
}
public function equals(mixed $other): bool {
if($other instanceof DateTimeInterface)
return $this->format(self::COMPARE) === $other->format(self::COMPARE);
return false;
}
public function isLessThan(DateTimeInterface $other): bool {
return $this->compare($other) < 0;
}
public function isLessThanOrEqual(DateTimeInterface $other): bool {
return $this->compare($other) <= 0;
}
public function isMoreThan(DateTimeInterface $other): bool {
return $this->compare($other) > 0;
}
public function isMoreThanOrEqual(DateTimeInterface $other): bool {
return $this->compare($other) >= 0;
}
/**
* Formats this DateTime as an ISO8601 date/time string.
*
* @return string This object represented as an ISO8601 date/time string.
*/
public function toISO8601String(): string {
return $this->format(DateTimeInterface::ATOM);
}
/**
* Formats this DateTime as a Cookie date/time string.
*
* @return string This object represented as a Cookie date/time string.
*/
public function toCookieString(): string {
return $this->format(DateTimeInterface::COOKIE);
}
/**
* Formats this DateTime as an RFC 822 date/time string.
*
* @return string This object represented as an RFC 822 date/time string.
*/
public function toRFC822String(): string {
return $this->format(DateTimeInterface::RFC822);
}
/**
* Creates a string representation of this object.
*
* @return string Representation of this object.
*/
public function __toString(): string {
return $this->format(DateTimeInterface::ATOM);
}
/**
* Returns the data which should be serialized as json.
*
* @see https://www.php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed Data to be passed to json_encode.
*/
public function jsonSerialize(): mixed {
return (string)$this;
}
/**
* Converts this immutable \Index\DateTime type to a mutable \DateTime type.
*
* @return \DateTime A new mutable \DateTime instance.
*/
public function toNative(): \DateTime {
return \DateTime::createFromImmutable($this);
}
/**
* Creates a DateTime object that is set to the current date and time, expressed in the provided time zone.
*
* @param DateTimeZone $timeZone Desired time zone, null for the current default time zone.
* @return DateTime An instance representing now.
*/
public static function now(?DateTimeZone $timeZone = null): DateTime {
return new DateTime('now', $timeZone);
}
/**
* Creates a DateTime object that is set to the current date and time, expressed in UTC.
*
* @return DateTime An instance representing now in UTC.
*/
public static function utcNow(): DateTime {
return self::now(TimeZoneInfo::utc());
}
/**
* Converts Unix time seconds to a DateTime object.
*
* @param int $seconds Unix time seconds.
* @return DateTime A DateTime instance representing the Unix time.
*/
public static function fromUnixTimeSeconds(int $seconds): DateTime {
return new DateTime('@' . $seconds);
}
/**
* Converts Unix time milliseconds to a DateTime object.
*
* @param float $millis Unix time milliseconds.
* @return DateTime A DateTime instance representing the Unix time.
*/
public static function fromUnixTimeMilliseconds(float $millis): DateTime {
return new DateTime('@' . ($millis / 1000));
}
/**
* Creates a new DateTime instance with the given components.
*
* @param int $year Desired year component.
* @param int $month Desired month component.
* @param int $day Desired day component.
* @param int $hour Desired hour component.
* @param int $minute Desired minute component.
* @param int $second Desired second component.
* @param int $micros Desired microsecond component.
* @param ?DateTimeZone $timeZone Desired time zone.
*/
public static function create(int $year, int $month = 1, int $day = 1, int $hour = 0, int $minute = 0, int $second = 0, int $micros = 0, ?DateTimeZone $timeZone = null): DateTime {
if($year < 1 || $year > 9999)
throw new InvalidArgumentException('$year may not be less than 1 or more than 9999.');
if($month < 1 || $month > 12)
throw new InvalidArgumentException('$month may not be less than 1 or more than 12.');
if($day < 1 || $day > 31)
throw new InvalidArgumentException('$day may not be less than 1 or more than 31.');
if($hour < 0 || $hour > 23)
throw new InvalidArgumentException('$hour may not be less than 0 or more than 23.');
if($minute < 0 || $minute > 59)
throw new InvalidArgumentException('$minute may not be less than 0 or more than 59.');
if($second < 0 || $second > 59)
throw new InvalidArgumentException('$second may not be less than 0 or more than 59.');
if($micros < 0 || $micros > 999999)
throw new InvalidArgumentException('$micros may not be less than 0 or more than 999999.');
return new DateTime(
sprintf(
'%04d-%02d-%02dT%02d:%02d:%02d.%06d',
$year, $month, $day, $hour, $minute, $second, $micros
),
$timeZone
);
}
/**
* Parses a time string and creates a DateTime using it.
*
* @see https://www.php.net/manual/en/datetime.createfromformat
* @param string $format Format that the passed string should be in.
* @param string $dateTime String representing the time.
* @param ?DateTimeZone $timeZone Desired time zone.
* @return DateTime|false A DateTime instance, or false on failure.
*/
public static function createFromFormat(string $format, string $dateTime, ?DateTimeZone $timeZone = null): DateTime|false {
$instance = parent::createFromFormat($format, $dateTime, $timeZone);
if($instance === false)
return false;
return self::cast($instance);
}
/**
* Creates a new DateTime instance from an interface implementation.
*
* @param DateTimeInterface $object Object that should be sourced.
* @return DateTime A new DateTime instance.
*/
public static function createFromInterface(DateTimeInterface $object): DateTime {
return self::cast($object);
}
/**
* Creates a new immutable DateTime instance from a mutable instance.
*
* @param \DateTime $object Mutable object that should be sourced.
* @return DateTime New immutable DateTime representing the same value.
*/
public static function createFromMutable(\DateTime $object): DateTime {
return self::cast($object);
}
/**
* @internal
*/
public static function __set_state(array $array): DateTime {
return self::cast(parent::__set_state($array));
}
/**
* Makes sure a DateTimeInterface implementing object is a DateTime instance.
*
* @param DateTimeInterface $dateTime Input object.
* @return DateTime If the input was a DateTime, the same instance will be returned.
* If the input was something else, a new DateTime instance will be returned based on the input.
*/
public static function cast(DateTimeInterface $dateTime): DateTime {
if($dateTime instanceof DateTime)
return $dateTime;
return new DateTime($dateTime->format('@U.u'), $dateTime->getTimezone());
}
}

340
src/Environment.php Normal file
View File

@ -0,0 +1,340 @@
<?php
// Environment.php
// Created: 2021-04-30
// Updated: 2022-02-27
namespace Index;
use RuntimeException;
/**
* Provides information about the current runtime environment.
*/
final class Environment {
/**
* Contains the end-of-line sequence for the current platform.
*
* @var string
*/
public const NEWLINE = PHP_EOL;
/**
* Contains the amount of bytes in an integer for this system.
*
* @var int
*/
public const INT_SIZE = PHP_INT_SIZE;
private static ?Version $indexVersion = null;
private static ?Version $phpVersion = null;
private static array $phpVersions = [];
/**
* Checks whether the current PHP environment is running in debug mode.
*
* Essentially checks if any sort of error reporting is enabled. It shouldn't be in prod.
*
* @return bool true if the environment is in debug mode, false is not.
*/
public static function isDebug(): bool {
return error_reporting() !== 0;
}
/**
* Toggles debug mode.
*
* Essentially turns all error reporting on or off.
*
* @param bool $debug true if debug mode should be enabled, false if it should be disabled.
*/
public static function setDebug(bool $debug = false): void {
if($debug) {
ini_set('display_errors', 'on');
error_reporting(-1);
} else {
ini_set('display_errors', 'off');
error_reporting(0);
}
}
/**
* Reads an environment variable.
*
* @param string $name Name of the environment variable.
* @param bool $localOnly true if only variables local to the application should be read,
* false if greater level operating system variables should be included.
* @return ?string null if the variable doesn't exist, otherwise a string of the value.
*/
public static function getVariable(string $name, bool $localOnly = true): ?string {
return ($value = getenv($name, $localOnly)) === false ? null : $value;
}
/**
* Writes an application-local environment variable.
*
* @param string $name Name of the environment variable.
* @param mixed $value Value that should be assigned to the environment variable. Will be cast to a string.
*/
public static function setVariable(string $name, mixed $value): void {
putenv($name . '=' . ((string)$value));
}
/**
* Removes an application-local environment variable.
*
* @param string $name Name of the environment variable.
*/
public static function removeVariable(string $name): void {
putenv($name);
}
/**
* Gets the current version of the Index library.
*
* Parses the VERSION in the root of the Index directory.
*
* @return Version A Version instance representing the version of the Index Library.
*/
public static function getIndexVersion(): Version {
if(self::$indexVersion === null)
self::$indexVersion = Version::parse(trim(file_get_contents(NDX_ROOT . DIRECTORY_SEPARATOR . 'VERSION')));
return self::$indexVersion;
}
/**
* Gets the version of the PHP installation Index is running on.
*
* @return Version A Version instance representing the version of PHP.
*/
public static function getPHPVersion(): Version {
if(self::$phpVersion === null)
self::$phpVersion = new Version(PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION);
return self::$phpVersion;
}
/**
* Gets the version of a PHP extension.
*
* @param string $extension Name of the extension.
* @return ?Version null if the extension is not installed, otherwise an instance of Version representing the version of the extension.
*/
public static function getPHPExtensionVersion(string $extension): ?Version {
if(!isset(self::$phpVersions[$extension])) {
$rawVersion = phpversion($extension);
self::$phpVersions[$extension] = empty($rawVersion) ? Version::empty() : Version::parse($rawVersion);
}
return self::$phpVersions[$extension];
}
/**
* Gets the name of the Operating System PHP is running on.
*
* @return string Name of the Operating System.
*/
public static function getOSName(): string {
static $value = null;
$value ??= php_uname('s');
return $value;
}
/**
* Checks if we're running on Windows.
*
* @return bool true if we're running on Windows, otherwise false.
*/
public static function isWindows(): bool {
return self::isWindowsNT()
|| self::getOSName() === 'Windows';
}
/**
* Checks if we're running on Windows NT.
*
* @return bool true if we're running on Windows NT, otherwise false.
*/
public static function isWindowsNT(): bool {
return ($os = self::getOSName()) === 'Windows NT'
|| $os === 'Windows_NT'
|| $os === 'WindowsNT';
}
/**
* Checks if we're running on a Unix-like Operating System.
*
* @return bool true if we're running on a Unix-like Operating System, otherwise false.
*/
public static function isUnixLike(): bool {
return self::isLinux()
|| self::isBSD()
|| self::isMacOS()
|| self::isSolaris();
}
/**
* Checks if we're running on a BSD derivative.
*
* @return bool true if we're running on a BSD derivative, otherwise false.
*/
public static function isBSD(): bool {
return self::isOpenBSD()
|| self::isFreeBSD()
|| self::isNetBSD()
|| self::isDragonFlyBSD()
|| self::getOSName() === 'BSD';
}
/**
* Checks if we're running on OpenBSD.
*
* @return bool true if we're running on OpenBSD, otherwise false.
*/
public static function isOpenBSD(): bool {
return self::getOSName() === 'OpenBSD';
}
/**
* Checks if we're running on FreeBSD.
*
* @return bool true if we're running on FreeBSD, otherwise false.
*/
public static function isFreeBSD(): bool {
return self::getOSName() === 'FreeBSD';
}
/**
* Checks if we're running on NetBSD.
*
* @return bool true if we're running on NetBSD, otherwise false.
*/
public static function isNetBSD(): bool {
return self::getOSName() === 'NetBSD';
}
/**
* Checks if we're running on DragonFly BSD.
*
* @return bool true if we're running on DragonFly BSD, otherwise false.
*/
public static function isDragonFlyBSD(): bool {
return self::getOSName() === 'DragonFly';
}
/**
* Checks if we're running on macOS.
*
* @return bool true if we're running on macOS, otherwise false.
*/
public static function isMacOS(): bool {
return self::getOSName() === 'Darwin';
}
/**
* Checks if we're running on Solaris.
*
* @return bool true if we're running on Solaris, otherwise false.
*/
public static function isSolaris(): bool {
return self::getOSName() === 'SunOS';
}
/**
* Checks if we're running on Linux.
*
* @return bool true if we're running on Linux, otherwise false.
*/
public static function isLinux(): bool {
return self::getOSName() === 'Linux';
}
/**
* Returns the name of the SAPI PHP is currently running through.
*
* @return string Name of the SAPI.
*/
public static function getSAPIName(): string {
static $sapi = null;
$sapi ??= php_sapi_name();
return $sapi;
}
/**
* Checks if we're running through Apache2Handler.
*
* @return bool true if we're running through Apache2Handler, otherwise false.
*/
public static function isApache2(): bool {
return ($sapi = self::getSAPIName()) === 'apache' || $sapi === 'apache2handler';
}
/**
* Checks if we're running through CGI.
*
* @return bool true if we're running through CGI, otherwise false.
*/
public static function isCGI(): bool {
return self::getSAPIName() === 'cgi';
}
/**
* Checks if we're running through FastCGI (and/or php-fpm).
*
* @return bool true if we're running through FastCGI, otherwise false.
*/
public static function isFastCGI(): bool {
return ($sapi = self::getSAPIName()) === 'cgi-fcgi' || $sapi === 'fpm-fcgi';
}
/**
* Checks if we're running through console.
*
* @return bool true if we're running through console, otherwise false.
*/
public static function isConsole(): bool {
return self::getSAPIName() === 'cli';
}
/**
* Checks if we're running through the built-in development server.
*
* @return bool true if we're running through the built-in development server, otherwise false.
*/
public static function isDebugServer(): bool {
return self::getSAPIName() === 'cli-server';
}
/**
* Checks if we're running through LiteSpeed.
*
* @return bool true if we're running through LiteSpeed, otherwise false.
*/
public static function isLiteSpeed(): bool {
return self::getSAPIName() === 'litespeed';
}
/**
* Checks if we're running through PHPDBG.
*
* @return bool true if we're running through PHPDBG, otherwise false.
*/
public static function isPHPDebugger(): bool {
return self::getSAPIName() === 'phpdbg';
}
/**
* Checks if we're running on a 32-bit system.
*
* @return bool true if we're on 32-bit, false if not.
*/
public static function is32Bit(): bool {
return PHP_INT_SIZE === 4;
}
/**
* Checks if we're running on a 64-bit system.
*
* @return bool true if we're on 64-bit, false if not.
*/
public static function is64Bit(): bool {
return PHP_INT_SIZE === 8;
}
}

79
src/Exceptions.php Normal file
View File

@ -0,0 +1,79 @@
<?php
// Exceptions.php
// Created: 2021-04-30
// Updated: 2021-05-12
namespace Index;
use ErrorException;
use Throwable;
/**
* Provides handling for uncaught exceptions and errors.
*/
final class Exceptions {
/**
* Convert errors to ErrorExceptions.
*
* Automatically invoked by inclusion of index.php into your project unless the constant <code>NDX_LEAVE_ERRORS</code> is defined beforehand.
* This is not recommended as it may cause undefined behaviour in some classes.
* This will also make error suppression not work, luckily you've not been using that since PHP 5. Right? Right?!
* Besides, this makes it possible to try..catch errors.
*/
public static function convertErrors(): void {
self::restoreErrors();
set_error_handler([self::class, 'handleError'], -1);
}
/**
* Restores error handling to the default PHP state.
*/
public static function restoreErrors(): void {
restore_error_handler();
}
/**
* Handle uncaught exceptions.
*
* Automatically invoked by inclusion of index.php into your project unless the constant <code>NDX_LEAVE_EXCEPTIONS</code> is defined.
*/
public static function handleExceptions(): void {
self::restoreExceptions();
//set_exception_handler([self::class, 'handleException']);
}
/**
* Restores uncaught exception handling to the default PHP state.
*/
public static function restoreExceptions(): void {
restore_exception_handler();
}
/**
* Converts errors to ErrorExceptions.
*
* Paramater documentation is copied from the set_error_handler page on php.net
*
* @see https://www.php.net/manual/en/function.set-error-handler.php
* @param int $errno The first parameter, errno, will be passed the level of the error raised, as an integer.
* @param string $errstr The second parameter, errstr, will be passed the error message, as a string.
* @param string $errfile If the callback accepts a third parameter, errfile, it will be passed the filename that the error was raised in, as a string.
* @param int $errline If the callback accepts a fourth parameter, errline, it will be passed the line number where the error was raised, as an integer.
* @throws ErrorException An ErrorException with the provided parameters.
* @return bool if this were false the PHP error handler would continue, but returning is never reached.
*/
public static function handleError(int $errno, string $errstr, string $errfile, int $errline): bool {
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
}
/**
* Handles uncaught exceptions.
*
* @see https://www.php.net/manual/en/function.set-exception-handler.php
* @param ?Throwable $ex Uncaught Throwable to handle. May be null to reset state(?) apparently.
*/
public static function handleException(?Throwable $ex): void {
if($ex === null)
return;
}
}

View File

@ -0,0 +1,48 @@
<?php
// BencodedContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
namespace Index\Http\Content;
use Stringable;
use Index\IO\Stream;
use Index\IO\FileStream;
use Index\Serialisation\Serialiser;
use Index\Serialisation\IBencodeSerialisable;
class BencodedContent implements Stringable, IHttpContent, IBencodeSerialisable {
private mixed $content;
public function __construct(mixed $content) {
$this->content = $content;
}
public function getContent(): mixed {
return $this->content;
}
public function bencodeSerialise(): mixed {
return $this->content;
}
public function encode(): string {
return Serialiser::bencode()->serialise($this->content);
}
public function __toString(): string {
return $this->encode();
}
public static function fromEncoded(Stream|string $encoded): BencodedContent {
return new BencodedContent(Serialiser::bencode()->deserialise($encoded));
}
public static function fromFile(string $path): BencodedContent {
return self::fromEncoded(FileStream::openRead($path));
}
public static function fromRequest(): BencodedContent {
return self::fromFile('php://input');
}
}

View File

@ -0,0 +1,54 @@
<?php
// FormContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
namespace Index\Http\Content;
use RuntimeException;
use Index\Http\HttpUploadedFile;
class FormContent implements IHttpContent {
private array $postFields;
private array $uploadedFiles;
public function __construct(array $postFields, array $uploadedFiles) {
$this->postFields = $postFields;
$this->uploadedFiles = $uploadedFiles;
}
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);
}
public function hasParam(string $name): bool {
return isset($this->postFields[$name]);
}
public function getParams(): array {
return $this->postFields;
}
public function getUploadedFile(string $name): HttpUploadedFile {
if(!isset($this->uploadedFiles[$name]))
throw new RuntimeException('No file with name $name present.');
return $this->uploadedFiles[$name];
}
public static function fromRaw(array $post, array $files): FormContent {
return new FormContent(
$post,
HttpUploadedFile::createFromFILES($files)
);
}
public static function fromRequest(): FormContent {
return self::fromRaw($_POST, $_FILES);
}
public function __toString(): string {
return '';
}
}

View File

@ -0,0 +1,11 @@
<?php
// IHttpContent.php
// Created: 2022-02-08
// Updated: 2022-02-27
namespace Index\Http\Content;
use Stringable;
interface IHttpContent extends Stringable {
}

View File

@ -0,0 +1,48 @@
<?php
// JsonContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
namespace Index\Http\Content;
use JsonSerializable;
use Stringable;
use Index\IO\Stream;
use Index\IO\FileStream;
use Index\Serialisation\Serialiser;
class JsonContent implements Stringable, IHttpContent, JsonSerializable {
private mixed $content;
public function __construct(mixed $content) {
$this->content = $content;
}
public function getContent(): mixed {
return $this->content;
}
public function jsonSerialize(): mixed {
return $this->content;
}
public function encode(): string {
return Serialiser::json()->serialise($this->content);
}
public function __toString(): string {
return $this->encode();
}
public static function fromEncoded(Stream|string $encoded): JsonContent {
return new JsonContent(Serialiser::json()->deserialise($encoded));
}
public static function fromFile(string $path): JsonContent {
return self::fromEncoded(FileStream::openRead($path));
}
public static function fromRequest(): JsonContent {
return self::fromFile('php://input');
}
}

View File

@ -0,0 +1,36 @@
<?php
// StreamContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
namespace Index\Http\Content;
use Stringable;
use Index\IO\Stream;
use Index\IO\FileStream;
class StreamContent implements Stringable, IHttpContent {
private Stream $stream;
public function __construct(Stream $stream) {
$this->stream = $stream;
}
public function getStream(): Stream {
return $this->stream;
}
public function __toString(): string {
return (string)$this->stream;
}
public static function fromFile(string $path): StreamContent {
return new StreamContent(
FileStream::openRead($path)
);
}
public static function fromRequest(): StreamContent {
return self::fromFile('php://input');
}
}

View File

@ -0,0 +1,36 @@
<?php
// StringContent.php
// Created: 2022-02-10
// Updated: 2022-02-27
namespace Index\Http\Content;
use Stringable;
class StringContent implements Stringable, IHttpContent {
private string $string;
public function __construct(string $string) {
$this->string = $string;
}
public function getString(): string {
return $this->string;
}
public function __toString(): string {
return $this->string;
}
public static function fromObject(string $string): StringContent {
return new StringContent($string);
}
public static function fromFile(string $path): StringContent {
return new StringContent(file_get_contents($path));
}
public static function fromRequest(): StringContent {
return self::fromFile('php://input');
}
}

View File

@ -0,0 +1,71 @@
<?php
// AcceptEncodingHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use stdClass;
use InvalidArgumentException;
class AcceptEncodingHeader {
private array $encodings;
private array $rejects;
public function __construct(array $encodings, array $rejects) {
$this->encodings = $encodings;
$this->rejects = $rejects;
}
public function getEncodings(): array {
return $this->encodings;
}
public function getRejects(): array {
return $this->rejects;
}
public function accepts(string $algoName): int {
$algoName = strtolower($algoName);
foreach($this->encodings as $algo)
if($algo->quality !== 0 && ($algo->name === $algoName || $algo->name === '*'))
return $algo->quality;
return 0;
}
public function rejects(string $locale): bool {
return in_array(strtolower($locale), $this->rejects)
|| in_array('*', $this->rejects);
}
public static function parse(array $lines): AcceptEncodingHeader {
$parts = explode(',', (string)$lines[0]);
$algos = [];
$rejects = [];
foreach($parts as $part)
try {
$part = explode(';', $part);
$algo = new stdClass;
$algo->name = strtolower(trim(array_shift($part)));
$algo->quality = 1;
foreach($part as $param) {
if(substr($param, 0, 2) === 'q=') {
$algo->quality = min(0, max(1, floatval(substr($param, 2))));
break;
}
}
$algos[] = $algo;
if($algo->quality === 0)
$rejects[] = $algo->name;
} catch(InvalidArgumentException $ex) {}
if(empty($algos))
throw new InvalidArgumentException('Failed to parse Accept-Encoding header.');
return new AcceptEncodingHeader($algos, $rejects);
}
}

View File

@ -0,0 +1,63 @@
<?php
// AcceptHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use InvalidArgumentException;
use Index\MediaType;
use Index\Http\HttpHeader;
class AcceptHeader {
private array $types;
private array $rejects;
public function __construct(array $types, array $rejects) {
$this->types = $types;
$this->rejects = $rejects;
}
public function getTypes(): array {
return $this->types;
}
public function getRejects(): array {
return $this->rejects;
}
public function accepts(MediaType|string $mediaType): ?MediaType {
foreach($this->types as $type)
if($type->getQuality() < 0.1 && $type->equals($mediaType))
return $type;
return null;
}
public function rejects(MediaType|string $mediaType): bool {
foreach($this->rejects as $reject)
if($reject->equals($mediaType))
return true;
return false;
}
public static function parse(HttpHeader $header): AcceptHeader {
$parts = explode(',', $header->getFirstLine());
$types = [];
$rejects = [];
foreach($parts as $part)
try {
$types[] = $type = MediaType::parse(trim($part));
if($type->getQuality() < 0.1)
$rejects[] = $type;
} catch(InvalidArgumentException $ex) {}
if(empty($types))
throw new InvalidArgumentException('Failed to parse Accept header.');
return new AcceptHeader($types, $rejects);
}
}

View File

@ -0,0 +1,72 @@
<?php
// AcceptLanguageHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use stdClass;
use InvalidArgumentException;
use Index\Http\HttpHeader;
class AcceptLanguageHeader {
private array $langs;
private array $rejects;
public function __construct(array $langs, array $rejects) {
$this->langs = $langs;
$this->rejects = $rejects;
}
public function getLanguages(): array {
return $this->langs;
}
public function getRejects(): array {
return $this->rejects;
}
public function accepts(string $locale): int {
$locale = strtolower($locale);
foreach($this->langs as $lang)
if($lang->quality !== 0 && ($lang->name === $locale || $lang->name === '*'))
return $lang->quality;
return 0;
}
public function rejects(string $locale): bool {
return in_array(strtolower($locale), $this->rejects)
|| in_array('*', $this->rejects);
}
public static function parse(HttpHeader $header): AcceptLanguageHeader {
$parts = explode(',', $header->getFirstLine());
$langs = [];
$rejects = [];
foreach($parts as $part)
try {
$part = explode(';', $part);
$lang = new stdClass;
$lang->name = strtolower(trim(array_shift($part)));
$lang->quality = 1;
foreach($part as $param) {
if(substr($param, 0, 2) === 'q=') {
$lang->quality = min(0, max(1, floatval(substr($param, 2))));
break;
}
}
$langs[] = $lang;
if($lang->quality === 0)
$rejects[] = $lang->name;
} catch(InvalidArgumentException $ex) {}
if(empty($langs))
throw new InvalidArgumentException('Failed to parse Accept-Language header.');
return new AcceptLanguageHeader($langs, $rejects);
}
}

View File

@ -0,0 +1,71 @@
<?php
// AcceptTransferEncodingHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use stdClass;
use InvalidArgumentException;
class AcceptTransferEncodingHeader {
private array $encodings;
private array $rejects;
public function __construct(array $encodings, array $rejects) {
$this->encodings = $encodings;
$this->rejects = $rejects;
}
public function getEncodings(): array {
return $this->encodings;
}
public function getRejects(): array {
return $this->rejects;
}
public function accepts(string $algoName): int {
$algoName = strtolower($algoName);
foreach($this->encodings as $algo)
if($algo->quality !== 0 && ($algo->name === $algoName || $algo->name === '*'))
return $algo->quality;
return 0;
}
public function rejects(string $locale): bool {
return in_array(strtolower($locale), $this->rejects)
|| in_array('*', $this->rejects);
}
public static function parse(array $lines): AcceptTransferEncodingHeader {
$parts = explode(',', (string)$lines[0]);
$algos = [];
$rejects = [];
foreach($parts as $part)
try {
$part = explode(';', $part);
$algo = new stdClass;
$algo->name = strtolower(trim(array_shift($part)));
$algo->quality = 1;
foreach($part as $param) {
if(substr($param, 0, 2) === 'q=') {
$algo->quality = min(0, max(1, floatval(substr($param, 2))));
break;
}
}
$algos[] = $algo;
if($algo->quality === 0)
$rejects[] = $algo->name;
} catch(InvalidArgumentException $ex) {}
if(empty($algos))
throw new InvalidArgumentException('Failed to parse TE header.');
return new AcceptTransferEncodingHeader($algos, $rejects);
}
}

View File

@ -0,0 +1,34 @@
<?php
// AccessControlRequestHeadersHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class AccessControlRequestHeadersHeader {
private array $headers;
public function __construct(array $headers) {
$this->headers = $headers;
}
public function getHeaders(): array {
return $this->headers;
}
public function isAllowed(string $header): bool {
return in_array(strtolower($header), $this->headers);
}
public static function parse(HttpHeader $header): AccessControlRequestHeadersHeader {
$raw = explode(',', $header->getFirstLine());
$headers = [];
foreach($raw as $header)
$headers[] = strtolower(trim($header));
return new AccessControlRequestHeadersHeader(array_unique($headers));
}
}

View File

@ -0,0 +1,28 @@
<?php
// AccessControlRequestMethodHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class AccessControlRequestMethodHeader {
private string $method;
public function __construct(string $method) {
$this->method = $method;
}
public function getMethod(): string {
return $this->method;
}
public function isMethod(string $method): bool {
return $this->method === strtolower($method);
}
public static function parse(HttpHeader $header): AccessControlRequestMethodHeader {
return new AccessControlRequestMethodHeader(strtolower(trim($header->getFirstLine())));
}
}

View File

@ -0,0 +1,35 @@
<?php
// AuthorizationHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class AuthorizationHeader {
private string $method;
private string $params;
public function __construct(string $method, string $params) {
$this->method = $method;
$this->params = $params;
}
public function getMethod(): string {
return $this->method;
}
public function isMethod(string $method): bool {
return $this->method === strtolower($method);
}
public function getParams(): string {
return $this->params;
}
public static function parse(HttpHeader $header): AuthorizationHeader {
$parts = explode(' ', trim($header->getFirstLine()), 2);
return new AuthorizationHeader($parts[0], $parts[1] ?? '');
}
}

View File

@ -0,0 +1,47 @@
<?php
// CacheControlHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class CacheControlHeader {
private array $params;
public function __construct(array $params) {
$this->params = $params;
}
public function getParams(): array {
return $this->params;
}
public function hasParam(string $name): bool {
return isset($this->params[strtolower($name)]);
}
public function getParam(string $name, int $filter = FILTER_DEFAULT, array|int $options = 0): mixed {
$name = strtolower($name);
if(!isset($this->params[$name]))
return null;
return filter_var($this->params[$name] ?? null, $filter, $options);
}
public static function parse(HttpHeader $header): CacheControlHeader {
$raw = explode(',', strtolower($header->getFirstLine()));
$params = [];
foreach($raw as $param) {
$parts = explode('=', $param, 2);
$name = trim($parts[0]);
$value = trim($parts[1] ?? '');
if($value === '')
$value = true;
$params[$name] = $value;
}
return new CacheControlHeader($params);
}
}

View File

@ -0,0 +1,35 @@
<?php
// ConnectionHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ConnectionHeader {
private array $params;
public function __construct(array $params) {
$this->params = $params;
}
public function getParams(): array {
return $this->params;
}
public function hasParam(string $directive): bool {
return in_array($directive, $this->params);
}
public static function parse(HttpHeader $header): ConnectionHeader {
return new ConnectionHeader(
array_unique(
array_map(
fn($directive) => trim($directive),
explode(',', strtolower($header->getFirstLine()))
)
)
);
}
}

View File

@ -0,0 +1,29 @@
<?php
// ContentEncodingHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ContentEncodingHeader {
private array $encodings;
public function __construct(array $encodings) {
$this->encodings = $encodings;
}
public function getEncodings(): array {
return $this->encodings;
}
public static function parse(HttpHeader $header): ContentEncodingHeader {
return new ContentEncodingHeader(
array_map(
fn($directive) => trim($directive),
explode(',', strtolower($header->getFirstLine()))
)
);
}
}

View File

@ -0,0 +1,35 @@
<?php
// ContentLanguageHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ContentLanguageHeader {
private array $langs;
public function __construct(array $langs) {
$this->langs = $langs;
}
public function getLanguages(): array {
return $this->langs;
}
public function hasLanguage(string $lang): bool {
return in_array(strtolower($lang), $this->langs);
}
public static function parse(HttpHeader $header): ContentLanguageHeader {
return new ContentLanguageHeader(
array_unique(
array_map(
fn($directive) => trim($directive),
explode(',', strtolower($header->getFirstLine()))
)
)
);
}
}

View File

@ -0,0 +1,24 @@
<?php
// ContentLengthHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ContentLengthHeader {
private int $length;
public function __construct(int $length) {
$this->length = $length;
}
public function getLength(): int {
return $this->length;
}
public static function parse(HttpHeader $header): ContentLengthHeader {
return new ContentLengthHeader(intval($header->getFirstLine()));
}
}

View File

@ -0,0 +1,24 @@
<?php
// ContentLocationHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ContentLocationHeader {
private string $location;
public function __construct(string $location) {
$this->location = $location;
}
public function getLocation(): string {
return $this->location;
}
public static function parse(HttpHeader $header): ContentLocationHeader {
return new ContentLocationHeader(trim($header->getFirstLine()));
}
}

View File

@ -0,0 +1,74 @@
<?php
// ContentRangeHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ContentRangeHeader {
private string $unit;
private int $rangeStart = -1;
private int $rangeEnd = -1;
private int $size = -1;
public function __construct(string $unit, int $rangeStart, int $rangeEnd, int $size) {
$this->unit = $unit;
$this->rangeStart = $rangeStart;
$this->rangeEnd = $rangeEnd;
$this->size = $size;
}
public function getUnit(): string {
return $this->unit;
}
public function isBytes(): bool {
return $this->unit === 'bytes';
}
public function getSize(): int {
return $this->size;
}
public function isUnknownSize(): bool {
return $this->size < 0;
}
public function isUnspecifiedRange(): bool {
return $this->rangeStart < 0 || $this->rangeEnd < 0;
}
public function getRangeStart(): int {
return $this->rangeStart;
}
public function getRangeEnd(): int {
return $this->rangeEnd;
}
public static function parse(HttpHeader $header): ContentRangeHeader {
$parts = explode(' ', trim($header->getFirstLine()), 2);
$unit = array_shift($parts);
$parts = explode('/', $parts[1] ?? '', 2);
$size = trim($parts[1] ?? '*');
if($size !== '*')
$size = max(0, intval($size));
else
$size = -1;
$range = trim($parts[0]);
if($range !== '*') {
$parts = explode('-', $range, 2);
$rangeStart = intval(trim($parts[0]));
$rangeEnd = intval(trim($parts[1] ?? '0'));
} else
$rangeStart = $rangeEnd = -1;
return new ContentRangeHeader($unit, $rangeStart, $rangeEnd, $size);
}
}

View File

@ -0,0 +1,25 @@
<?php
// ContentTypeHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\MediaType;
use Index\Http\HttpHeader;
class ContentTypeHeader {
private MediaType $mediaType;
public function __construct(MediaType $mediaType) {
$this->mediaType = $mediaType;
}
public function getMediaType(): MediaType {
return $this->mediaType;
}
public static function parse(HttpHeader $header): ContentTypeHeader {
return new ContentTypeHeader(MediaType::parse($header->getFirstLine()));
}
}

View File

@ -0,0 +1,47 @@
<?php
// CookieHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\UrlEncoding;
use Index\Http\HttpHeader;
class CookieHeader {
private array $cookies;
public function __construct(array $cookies) {
$this->cookies = $cookies;
}
public function getCookies(): array {
return $this->cookies;
}
public function hasCookie(string $name): bool {
return isset($this->cookies[$name]);
}
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);
}
public static function parse(HttpHeader $header): CookieHeader {
$cookies = [];
$lines = $header->getLines();
foreach($lines as $line) {
$parts = explode(';', $line);
foreach($parts as $part) {
$kvp = explode('=', $part, 2);
$cookies[rawurldecode($kvp[0])] = rawurldecode($kvp[1] ?? '');
}
}
return new CookieHeader($cookies);
}
}

View File

@ -0,0 +1,33 @@
<?php
// DNTHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class DNTHeader {
private ?bool $state;
public function __construct(?bool $state) {
$this->state = $state;
}
public function getState(): ?bool {
return $this->state;
}
public function hasNoPreference(): bool {
return $this->state === null;
}
public function allowsTracking(): bool {
return $this->state !== false;
}
public static function parse(HttpHeader $header): DNTHeader {
$value = $header->getFirstLine();
return new DNTHeader($value === 'null' ? null : ($value === '1'));
}
}

View File

@ -0,0 +1,25 @@
<?php
// DateHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\DateTime;
use Index\Http\HttpHeader;
class DateHeader {
private DateTime $dateTime;
public function __construct(DateTime $dateTime) {
$this->dateTime = $dateTime;
}
public function getDateTime(): DateTime {
return $this->dateTime;
}
public static function parse(HttpHeader $header): DateHeader {
return new DateHeader(DateTime::createFromFormat(\DateTimeInterface::RFC7231, $header->getFirstLine()));
}
}

View File

@ -0,0 +1,31 @@
<?php
// ExpectHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ExpectHeader {
private int $statusCode;
private string $statusText;
public function __construct(int $statusCode, string $statusText) {
$this->statusCode = $statusCode;
$this->statusText = $statusText;
}
public function getStatusCode(): int {
return $this->statusCode;
}
public function getStatusText(): string {
return $this->statusText;
}
public static function parse(HttpHeader $header): ExpectHeader {
$value = explode('-', $header->getFirstLine(), 2);
return new ExpectHeader(intval($value[0]), $value[1] ?? '');
}
}

View File

@ -0,0 +1,24 @@
<?php
// FromHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class FromHeader {
private string $email;
public function __construct(string $email) {
$this->email = $email;
}
public function getEMailAddress(): string {
return $this->email;
}
public static function parse(HttpHeader $header): FromHeader {
return new FromHeader($header->getFirstLine());
}
}

View File

@ -0,0 +1,35 @@
<?php
// HostHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class HostHeader {
private string $host;
private int $port;
public function __construct(string $host, int $port) {
$this->host = $host;
$this->port = $port;
}
public function getHost(): string {
return $this->host;
}
public function hasPort(): bool {
return $this->port > 0;
}
public function getPort(): int {
return $this->port;
}
public static function parse(HttpHeader $header): HostHeader {
$parts = explode(':', $header->getFirstLine(), 2);
return new HostHeader($parts[0], intval($parts[1] ?? '-1'));
}
}

View File

@ -0,0 +1,46 @@
<?php
// IfMatchHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class IfMatchHeader {
private array $tags;
private int $count;
public function __construct(array $tags) {
$this->tags = $tags;
$this->count = count($tags);
}
public function getTags(): array {
return $this->tags;
}
public function hasTag(string $tag): bool {
if($this->count === 1 && $this->tags[0] === '*')
return true;
return in_array((string)$tag, $this->tags);
}
public static function parse(HttpHeader $header): IfMatchHeader {
$tags = [];
$rawTags = array_unique(
array_map(
fn($directive) => trim($directive),
explode(',', strtolower($header->getFirstLine()))
)
);
foreach($rawTags as $raw) {
if(substr($raw, 0, 3) === 'W/"')
continue;
$tags[] = trim($raw, '"');
}
return new IfMatchHeader($tags);
}
}

View File

@ -0,0 +1,30 @@
<?php
// IfModifiedSinceHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use DateTimeInterface;
use Index\DateTime;
use Index\Http\HttpHeader;
class IfModifiedSinceHeader {
private DateTime $dateTime;
public function __construct(DateTime $dateTime) {
$this->dateTime = $dateTime;
}
public function getDateTime(): DateTime {
return $this->dateTime;
}
public function isLessThanOrEqual(DateTimeInterface $dateTime): bool {
return $this->dateTime->isLessThanOrEqual($dateTime);
}
public static function parse(HttpHeader $header): IfModifiedSinceHeader {
return new IfModifiedSinceHeader(DateTime::createFromFormat(\DateTimeInterface::RFC7231, $header->getFirstLine()));
}
}

View File

@ -0,0 +1,46 @@
<?php
// IfNoneMatchHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class IfNoneMatchHeader {
private array $tags;
private int $count;
public function __construct(array $tags) {
$this->tags = $tags;
$this->count = count($tags);
}
public function getTags(): array {
return $this->tags;
}
public function hasTag(string $tag): bool {
if($this->count === 1 && $this->tags[0] === '*')
return true;
return in_array($tag, $this->tags);
}
public static function parse(HttpHeader $header): IfNoneMatchHeader {
$tags = [];
$rawTags = array_unique(
array_map(
fn($directive) => trim($directive),
explode(',', strtolower($header->getFirstLine()))
)
);
foreach($rawTags as $raw) {
if(str_starts_with($raw, 'W/"'))
$raw = substr($raw, 2);
$tags[] = trim($raw, '"');
}
return new IfNoneMatchHeader($tags);
}
}

View File

@ -0,0 +1,58 @@
<?php
// IfRangeHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use DateTimeInterface;
use Index\DateTime;
use Index\Http\HttpHeader;
class IfRangeHeader {
private ?string $tag;
private ?DateTime $dateTime;
public function __construct(?string $tag, ?DateTime $dateTime) {
$this->tag = $tag;
$this->dateTime = $dateTime;
}
public function hasTag(): bool {
return $this->tag !== null;
}
public function getTag(): string {
return $this->tag;
}
public function hasDateTime(): bool {
return $this->dateTime !== null;
}
public function getDateTime(): DateTime {
return $this->dateTime;
}
public function matches(string|DateTimeInterface $other): bool {
if($this->hasTag() && is_string($other)) {
if(str_starts_with($other, 'W/"'))
return false;
return $this->tag === $other;
}
if($this->hasDateTime() && $other instanceof DateTimeInterface)
return $this->dateTime->isLessThanOrEqual($other);
return false;
}
public static function parse(HttpHeader $header): IfRangeHeader {
$line = $header->getFirstLine();
if($line[0] !== '"' && !str_starts_with($line, 'W/"'))
return new IfRangeHeader(null, DateTime::createFromFormat(\DateTimeInterface::RFC7231, $line));
return new IfRangeHeader($line, null);
}
}

View File

@ -0,0 +1,30 @@
<?php
// IfUnmodifiedSinceHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use DateTimeInterface;
use Index\DateTime;
use Index\Http\HttpHeader;
class IfUnmodifiedSinceHeader {
private DateTime $dateTime;
public function __construct(DateTime $dateTime) {
$this->dateTime = $dateTime;
}
public function getDateTime(): DateTime {
return $this->dateTime;
}
public function isMoreThan(DateTimeInterface $dateTime): bool {
return $this->dateTime->isMoreThan($dateTime);
}
public static function parse(HttpHeader $header): IfUnmodifiedSinceHeader {
return new IfUnmodifiedSinceHeader(DateTime::createFromFormat(\DateTimeInterface::RFC7231, $header->getFirstLine()));
}
}

View File

@ -0,0 +1,43 @@
<?php
// KeepAliveHeader.php
// Created: 2022-02-14
// Updated: 2022-02-15
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class KeepAliveHeader {
private int $timeOut;
private int $max;
public function __construct(int $timeOut, int $max) {
$this->timeOut = $timeOut;
$this->max = $max;
}
public function getTimeOut(): int {
return $this->timeOut;
}
public function getMax(): int {
return $this->max;
}
public static function parse(HttpHeader $header): KeepAliveHeader {
$params = explode(',', $header->getFirstLine());
$timeOut = -1;
$max = -1;
foreach($params as $param) {
if(str_starts_with($param, 'timeout='))
$timeOut = intval(substr($param, 8));
elseif(str_starts_with($param, 'max='))
$max = intval(substr($param, 4));
if($timeOut >= 0 && $max >= 0)
break;
}
return new KeepAliveHeader($timeOut, $max);
}
}

View File

@ -0,0 +1,25 @@
<?php
// LastModifiedHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\DateTime;
use Index\Http\HttpHeader;
class LastModifiedHeader {
private DateTime $dateTime;
public function __construct(DateTime $dateTime) {
$this->dateTime = $dateTime;
}
public function getDateTime(): DateTime {
return $this->dateTime;
}
public static function parse(HttpHeader $header): LastModifiedHeader {
return new LastModifiedHeader(DateTime::createFromFormat(\DateTimeInterface::RFC7231, $header->getFirstLine()));
}
}

View File

@ -0,0 +1,61 @@
<?php
// OriginHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\DateTime;
use Index\Http\HttpHeader;
class OriginHeader {
private bool $isNull;
private string $scheme;
private string $host;
private int $port;
public function __construct(bool $isNull, string $scheme, string $host, int $port) {
$this->isNull = $isNull;
$this->scheme = $scheme;
$this->host = $host;
$this->port = $port;
}
public function isNull(): bool {
return $this->isNull;
}
public function isValid(): bool {
return $this->isNull || ($this->scheme !== '' && $this->host !== '');
}
public function getScheme(): string {
return $this->scheme;
}
public function getHost(): string {
return $this->host;
}
public function hasPort(): bool {
return $this->port > 0;
}
public function getPort(): int {
return $this->port;
}
public static function parse(HttpHeader $header): OriginHeader {
$line = $header->getFirstLine();
if($line === 'null')
return new OriginHeader(true, '', '', -1);
return new OriginHeader(
false,
parse_url($line, PHP_URL_SCHEME) ?? '',
parse_url($line, PHP_URL_HOST) ?? '',
parse_url($line, PHP_URL_PORT) ?? -1
);
}
}

View File

@ -0,0 +1,24 @@
<?php
// PragmaHeader.php
// Created: 2022-02-14
// Updated: 2022-02-14
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class PragmaHeader {
private bool $noCache;
public function __construct(bool $noCache) {
$this->noCache = $noCache;
}
public function shouldByPassCache(): bool {
return $this->noCache;
}
public static function parse(HttpHeader $header): PragmaHeader {
return new PragmaHeader($header->getFirstLine() === 'no-cache');
}
}

View File

@ -0,0 +1,35 @@
<?php
// ProxyAuthorizationHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class ProxyAuthorizationHeader {
private string $method;
private string $params;
public function __construct(string $method, string $params) {
$this->method = $method;
$this->params = $params;
}
public function getMethod(): string {
return $this->method;
}
public function isMethod(string $method): bool {
return $this->method === strtolower($method);
}
public function getParams(): string {
return $this->params;
}
public static function parse(HttpHeader $header): ProxyAuthorizationHeader {
$parts = explode(' ', trim($header->getFirstLine()), 2);
return new ProxyAuthorizationHeader($parts[0], $parts[1] ?? '');
}
}

View File

@ -0,0 +1,61 @@
<?php
// RangeHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use stdClass;
use Index\Http\HttpHeader;
class RangeHeader {
private string $unit;
private array $ranges;
public function __construct(string $unit, array $ranges) {
$this->unit = $unit;
$this->ranges = $ranges;
}
public function getUnit(): string {
return $this->unit;
}
public function isBytes(): bool {
return $this->unit === 'bytes';
}
public function getRanges(): array {
return $this->ranges;
}
public static function parse(HttpHeader $header): RangeHeader {
$parts = explode('=', trim($header->getFirstLine()), 2);
$unit = trim($parts[0]);
$ranges = [];
$rawRanges = explode(',', $parts[1] ?? '', 2);
foreach($rawRanges as $raw) {
$raw = explode('-', trim($raw), 2);
$start = trim($raw[0]);
$end = trim($raw[1] ?? '');
$ranges[] = $range = new stdClass;
if($start === '' && is_numeric($end)) {
$range->type = 'suffix-length';
$range->length = intval($end);
} elseif(is_numeric($start) && $end === '') {
$range->type = 'start';
$range->start = intval($start);
} else {
$range->type = 'range';
$range->start = intval($start);
$range->end = intval($end);
}
}
return new RangeHeader($unit, $ranges);
}
}

View File

@ -0,0 +1,24 @@
<?php
// RefererHeader.php
// Created: 2022-02-14
// Updated: 2022-02-27
namespace Index\Http\Headers;
use Index\Http\HttpHeader;
class RefererHeader {
private string $url;
public function __construct(string $url) {
$this->url = $url;
}
public function getSource(): string {
return $this->url;
}
public static function parse(HttpHeader $header): RefererHeader {
return new RefererHeader(trim($header->getFirstLine()));
}
}

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