Imported into new repository.

master
flash 6 months ago
commit ac2255d24d

5
.gitattributes vendored

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

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

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

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

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

@ -0,0 +1 @@
0.2202.281831

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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