commit
ac2255d24d
@ -0,0 +1,5 @@
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.php text eol=lf
|
||||
*.bat text eol=crlf
|
||||
VERSION text eol=lf
|
@ -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,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.');
|
||||
}
|
||||
|
||||