Initial commit.
This commit is contained in:
commit
8a849213e4
31 changed files with 3963 additions and 0 deletions
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.php text eol=lf
|
||||
*.bat text eol=crlf
|
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
[Tt]humbs.db
|
||||
[Dd]esktop.ini
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.vs/
|
||||
.idea/
|
||||
docs/html/
|
||||
.phpdoc*
|
||||
.phpunit*
|
||||
vendor/
|
30
LICENCE
Normal file
30
LICENCE
Normal file
|
@ -0,0 +1,30 @@
|
|||
Copyright (c) 2023, flashwave <me@flash.moe>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted (subject to the limitations in the disclaimer
|
||||
below) provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
* 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.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from this
|
||||
software without specific prior written permission.
|
||||
|
||||
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY
|
||||
THIS LICENSE. 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.
|
35
README.md
Normal file
35
README.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# Syokuhou
|
||||
|
||||
Syokuhou is a common library for configuration in my PHP projects.
|
||||
It provides both a file interface and a database interface out of the box as well as scoping.
|
||||
|
||||
|
||||
## Requirements and Dependencies
|
||||
|
||||
Syokuhou currently targets **PHP 8.2**.
|
||||
|
||||
### `Path to database interface`
|
||||
|
||||
A compatible `Index\Data\IDbConnection` must be provided.
|
||||
|
||||
|
||||
## Versioning
|
||||
|
||||
Syokuhou versioning will follows the [Semantic Versioning specification v2.0.0](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
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 Syokuhou using `Syokuhou\SyokuhouInfo::getVersion()`.
|
||||
|
||||
|
||||
## Contribution
|
||||
|
||||
By submitting code for inclusion in the main Syokuhou 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
|
||||
|
||||
Syokuhou is available under the BSD 3-Clause Clear License, a full version of which is enclosed in the LICENCE file.
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
1.0.0-dev
|
30
composer.json
Normal file
30
composer.json
Normal file
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"name": "flashwave/syokuhou",
|
||||
"description": "Configuration library for PHP.",
|
||||
"type": "library",
|
||||
"homepage": "https://railgun.sh/syokuhou",
|
||||
"license": "bsd-3-clause-clear",
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true,
|
||||
"require": {
|
||||
"php": ">=8.2",
|
||||
"flashwave/index": "dev-master"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.4",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "flashwave",
|
||||
"email": "packagist@flash.moe",
|
||||
"homepage": "https://flash.moe",
|
||||
"role": "mom"
|
||||
}
|
||||
],
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Syokuhou\\": "src"
|
||||
}
|
||||
}
|
||||
}
|
1745
composer.lock
generated
Normal file
1745
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
41
phpdoc.xml
Normal file
41
phpdoc.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<phpdocumentor configVersion="3.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://www.phpdoc.org"
|
||||
xsi:noNamespaceSchemaLocation="data/xsd/phpdoc.xsd">
|
||||
<title>Syokuhou 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>Syokuhou</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>
|
7
phpstan.neon
Normal file
7
phpstan.neon
Normal file
|
@ -0,0 +1,7 @@
|
|||
parameters:
|
||||
level: 9
|
||||
checkUninitializedProperties: true
|
||||
checkImplicitMixed: true
|
||||
checkBenevolentUnionTypes: true
|
||||
paths:
|
||||
- src
|
23
phpunit.xml
Normal file
23
phpunit.xml
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
|
||||
colors="true"
|
||||
executionOrder="depends,defects"
|
||||
beStrictAboutOutputDuringTests="true"
|
||||
failOnRisky="true"
|
||||
failOnWarning="true"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
requireCoverageMetadata="true"
|
||||
beStrictAboutCoverageMetadata="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory suffix="Test.php">tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage/>
|
||||
<source>
|
||||
<include>
|
||||
<directory suffix=".php">src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
226
src/DbConfig.php
Normal file
226
src/DbConfig.php
Normal file
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
// DbConfig.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\Data\DbStatementCache;
|
||||
use Index\Data\DbTools;
|
||||
use Index\Data\IDbConnection;
|
||||
|
||||
/**
|
||||
* Provides a configuration based on a {@see IDbConnection} instance.
|
||||
*
|
||||
* @todo provide table name in constructor
|
||||
* @todo scan for vendor specific queries and generalise them
|
||||
* @todo getValues() parsing should probably be done external so it can be reused
|
||||
*/
|
||||
class DbConfig implements IConfig {
|
||||
use MutableConfigTrait, GetValueInfoTrait, GetValuesTrait;
|
||||
|
||||
private DbStatementCache $cache;
|
||||
/** @var array<string, DbConfigValueInfo> */
|
||||
private array $values = [];
|
||||
|
||||
public function __construct(
|
||||
IDbConnection $dbConn,
|
||||
private string $tableName,
|
||||
private string $nameField = 'config_name',
|
||||
private string $valueField = 'config_value'
|
||||
) {
|
||||
$this->cache = new DbStatementCache($dbConn);
|
||||
}
|
||||
|
||||
public static function validateName(string $name): bool {
|
||||
// this should better validate the format, this allows for a lot of shittery
|
||||
return preg_match('#^([a-z][a-zA-Z0-9._]+)$#', $name) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets value cache.
|
||||
*/
|
||||
public function reset(): void {
|
||||
$this->values = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Unloads specifics items from the local cache.
|
||||
*
|
||||
* @param string|string[] $names Names of values to unload.
|
||||
*/
|
||||
public function unload(string|array $names): void {
|
||||
if(empty($names))
|
||||
return;
|
||||
if(is_string($names))
|
||||
$names = [$names];
|
||||
|
||||
foreach($names as $name)
|
||||
unset($this->values[$name]);
|
||||
}
|
||||
|
||||
public function getSeparator(): string {
|
||||
return '.';
|
||||
}
|
||||
|
||||
public function scopeTo(string ...$prefix): IConfig {
|
||||
return new ScopedConfig($this, $prefix);
|
||||
}
|
||||
|
||||
public function hasValues(string|array $names): bool {
|
||||
if(empty($names))
|
||||
return true;
|
||||
if(is_string($names))
|
||||
$names = [$names];
|
||||
|
||||
$cachedNames = array_keys($this->values);
|
||||
$names = array_diff($names, $cachedNames);
|
||||
|
||||
if(!empty($names)) {
|
||||
// array_diff preserves keys, the for() later would fuck up without it
|
||||
$names = array_values($names);
|
||||
$nameCount = count($names);
|
||||
|
||||
$stmt = $this->cache->get(sprintf(
|
||||
'SELECT COUNT(*) FROM %s WHERE %s IN (%s)',
|
||||
$this->tableName, $this->nameField,
|
||||
DbTools::prepareListString($nameCount)
|
||||
));
|
||||
for($i = 0; $i < $nameCount; ++$i)
|
||||
$stmt->addParameter($i + 1, $names[$i]);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
if($result->next())
|
||||
return $result->getInteger(0) >= $nameCount;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeValues(string|array $names): void {
|
||||
if(empty($names))
|
||||
return;
|
||||
if(is_string($names))
|
||||
$names = [$names];
|
||||
|
||||
foreach($names as $name)
|
||||
unset($this->values[$name]);
|
||||
|
||||
$nameCount = count($names);
|
||||
$stmt = $this->cache->get(sprintf(
|
||||
'DELETE FROM %s WHERE %s IN (%s)',
|
||||
$this->tableName, $this->nameField,
|
||||
DbTools::prepareListString($nameCount)
|
||||
));
|
||||
|
||||
for($i = 0; $i < $nameCount; ++$i)
|
||||
$stmt->addParameter($i + 1, $names[$i]);
|
||||
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
public function getAllValueInfos(int $range = 0, int $offset = 0): array {
|
||||
$this->reset();
|
||||
$infos = [];
|
||||
|
||||
$hasRange = $range !== 0;
|
||||
|
||||
$query = sprintf('SELECT %s, %s FROM %s', $this->nameField, $this->valueField, $this->tableName);
|
||||
if($hasRange) {
|
||||
if($range < 0)
|
||||
throw new InvalidArgumentException('$range must be a positive integer.');
|
||||
if($offset < 0)
|
||||
throw new InvalidArgumentException('$offset must be greater than zero if a range is specified.');
|
||||
|
||||
$query .= ' LIMIT ? OFFSET ?';
|
||||
}
|
||||
|
||||
$stmt = $this->cache->get($query);
|
||||
if($hasRange) {
|
||||
$stmt->addParameter(1, $range);
|
||||
$stmt->addParameter(2, $offset);
|
||||
}
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
|
||||
while($result->next()) {
|
||||
$name = $result->getString(0);
|
||||
$infos[] = $this->values[$name] = new DbConfigValueInfo($result);
|
||||
}
|
||||
|
||||
return $infos;
|
||||
}
|
||||
|
||||
public function getValueInfos(string|array $names): array {
|
||||
if(empty($names))
|
||||
return [];
|
||||
if(is_string($names))
|
||||
$names = [$names];
|
||||
|
||||
$infos = [];
|
||||
$skip = [];
|
||||
|
||||
foreach($names as $name)
|
||||
if(array_key_exists($name, $this->values)) {
|
||||
$infos[] = $this->values[$name];
|
||||
$skip[] = $name;
|
||||
}
|
||||
|
||||
$names = array_diff($names, $skip);
|
||||
|
||||
if(!empty($names)) {
|
||||
// array_diff preserves keys, the for() later would fuck up without it
|
||||
$names = array_values($names);
|
||||
$nameCount = count($names);
|
||||
|
||||
$stmt = $this->cache->get(sprintf(
|
||||
'SELECT %s, %s FROM %s WHERE config_name IN (%s)',
|
||||
$this->nameField, $this->valueField, $this->tableName,
|
||||
DbTools::prepareListString($nameCount)
|
||||
));
|
||||
for($i = 0; $i < $nameCount; ++$i)
|
||||
$stmt->addParameter($i + 1, $names[$i]);
|
||||
$stmt->execute();
|
||||
|
||||
$result = $stmt->getResult();
|
||||
while($result->next()) {
|
||||
$name = $result->getString(0);
|
||||
$infos[] = $this->values[$name] = new DbConfigValueInfo($result);
|
||||
}
|
||||
}
|
||||
|
||||
return $infos;
|
||||
}
|
||||
|
||||
public function setValues(array $values): void {
|
||||
if(empty($values))
|
||||
return;
|
||||
|
||||
$stmt = $this->cache->get(sprintf(
|
||||
'INSERT INTO %s (%s, %s) VALUES (?, ?)',
|
||||
$this->tableName, $this->nameField, $this->valueField
|
||||
));
|
||||
|
||||
foreach($values as $name => $value) {
|
||||
if(!self::validateName($name))
|
||||
throw new InvalidArgumentException('Invalid name encountered in $values.');
|
||||
|
||||
if(is_array($value)) {
|
||||
foreach($value as $entry)
|
||||
if(!is_scalar($entry))
|
||||
throw new InvalidArgumentException('An array value in $values contains a non-scalar type.');
|
||||
} elseif(!is_scalar($value))
|
||||
throw new InvalidArgumentException('Invalid value type encountered in $values.');
|
||||
|
||||
$this->removeValues($name);
|
||||
|
||||
$stmt->addParameter(1, $name);
|
||||
$stmt->addParameter(2, serialize($value));
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
91
src/DbConfigValueInfo.php
Normal file
91
src/DbConfigValueInfo.php
Normal file
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
// DbConfigValueInfo.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use UnexpectedValueException;
|
||||
use Index\Data\IDbResult;
|
||||
|
||||
/**
|
||||
* Provides information about a databased configuration value.
|
||||
*/
|
||||
class DbConfigValueInfo implements IConfigValueInfo {
|
||||
private string $name;
|
||||
private string $value;
|
||||
|
||||
/** @internal */
|
||||
public function __construct(IDbResult $result) {
|
||||
$this->name = $result->getString(0);
|
||||
$this->value = $result->getString(1);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return match($this->value[0]) {
|
||||
's' => 'string',
|
||||
'a' => 'array',
|
||||
'i' => 'int',
|
||||
'b' => 'bool',
|
||||
'd' => 'float',
|
||||
default => 'unknown',
|
||||
};
|
||||
}
|
||||
|
||||
public function isString(): bool { return $this->value[0] === 's'; }
|
||||
public function isInteger(): bool { return $this->value[0] === 'i'; }
|
||||
public function isFloat(): bool { return $this->value[0] === 'd'; }
|
||||
public function isBoolean(): bool { return $this->value[0] === 'b'; }
|
||||
public function isArray(): bool { return $this->value[0] === 'a'; }
|
||||
|
||||
public function getValue(): mixed {
|
||||
return unserialize($this->value);
|
||||
}
|
||||
|
||||
public function getString(): string {
|
||||
$value = $this->getValue();
|
||||
if(!is_string($value))
|
||||
throw new UnexpectedValueException('Value is not a string.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getInteger(): int {
|
||||
$value = $this->getValue();
|
||||
if(!is_int($value))
|
||||
throw new UnexpectedValueException('Value is not an integer.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getFloat(): float {
|
||||
$value = $this->getValue();
|
||||
if(!is_float($value))
|
||||
throw new UnexpectedValueException('Value is not a floating point number.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getBoolean(): bool {
|
||||
$value = $this->getValue();
|
||||
if(!is_bool($value))
|
||||
throw new UnexpectedValueException('Value is not a boolean.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function getArray(): array {
|
||||
$value = $this->getValue();
|
||||
if(!is_array($value))
|
||||
throw new UnexpectedValueException('Value is not an array.');
|
||||
return $value;
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
$value = $this->getValue();
|
||||
if(is_array($value))
|
||||
return implode(', ', $value);
|
||||
|
||||
return (string)$value; // @phpstan-ignore-line dude trust me
|
||||
}
|
||||
}
|
41
src/GetValueInfoTrait.php
Normal file
41
src/GetValueInfoTrait.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
// GetValueInfoTrait.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
/**
|
||||
* Provides implementations for things that are essentially macros for {@see IConfig::getValueInfos}.
|
||||
*/
|
||||
trait GetValueInfoTrait {
|
||||
public function getValueInfo(string $name): ?IConfigValueInfo {
|
||||
$infos = $this->getValueInfos($name);
|
||||
return empty($infos) ? null : $infos[0];
|
||||
}
|
||||
|
||||
public function getString(string $name, string $default = ''): string {
|
||||
$valueInfo = $this->getValueInfo($name);
|
||||
return $valueInfo?->isString() ? $valueInfo->getString() : $default;
|
||||
}
|
||||
|
||||
public function getInteger(string $name, int $default = 0): int {
|
||||
$valueInfo = $this->getValueInfo($name);
|
||||
return $valueInfo?->isInteger() ? $valueInfo->getInteger() : $default;
|
||||
}
|
||||
|
||||
public function getFloat(string $name, float $default = 0): float {
|
||||
$valueInfo = $this->getValueInfo($name);
|
||||
return $valueInfo?->isFloat() ? $valueInfo->getFloat() : $default;
|
||||
}
|
||||
|
||||
public function getBoolean(string $name, bool $default = false): bool {
|
||||
$valueInfo = $this->getValueInfo($name);
|
||||
return $valueInfo?->isBoolean() ? $valueInfo->getBoolean() : $default;
|
||||
}
|
||||
|
||||
public function getArray(string $name, array $default = []): array {
|
||||
$valueInfo = $this->getValueInfo($name);
|
||||
return $valueInfo?->isArray() ? $valueInfo->getArray() : $default;
|
||||
}
|
||||
}
|
96
src/GetValuesTrait.php
Normal file
96
src/GetValuesTrait.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
// GetValuesTrait.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Provides implementation for {@see IConfig::getValues} based on {@see IConfig::getValueInfos}.
|
||||
*/
|
||||
trait GetValuesTrait {
|
||||
/**
|
||||
* Format described in {@see IConfig::getValues}.
|
||||
*
|
||||
* @param array<string|string[]> $specs
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getValues(array $specs): array {
|
||||
$names = [];
|
||||
$evald = [];
|
||||
|
||||
foreach($specs as $key => $spec) {
|
||||
if(is_string($spec)) {
|
||||
$name = $spec;
|
||||
$default = null;
|
||||
$alias = null;
|
||||
} elseif(is_array($spec) && !empty($spec)) {
|
||||
$name = $spec[0];
|
||||
$default = $spec[1] ?? null;
|
||||
$alias = $spec[2] ?? null;
|
||||
} else
|
||||
throw new InvalidArgumentException('$specs array contains an invalid entry.');
|
||||
|
||||
$nameLength = strlen($name);
|
||||
if($nameLength > 3 && ($colon = strrpos($name, ':')) === $nameLength - 2) {
|
||||
$type = substr($name, $colon + 1, 1);
|
||||
$name = substr($name, 0, $colon);
|
||||
} else $type = '';
|
||||
|
||||
$names[] = $name;
|
||||
$evald[$key] = [
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'default' => $default,
|
||||
'alias' => $alias,
|
||||
];
|
||||
}
|
||||
|
||||
$infos = $this->getValueInfos($names);
|
||||
$results = [];
|
||||
|
||||
foreach($evald as $spec) {
|
||||
foreach($infos as $infoTest)
|
||||
if($infoTest->getName() === $spec['name']) {
|
||||
$info = $infoTest;
|
||||
break;
|
||||
}
|
||||
|
||||
$resultName = $spec['alias'] ?? $spec['name'];
|
||||
|
||||
if(!isset($info)) {
|
||||
$defaultValue = $spec['default'] ?? null;
|
||||
if($spec['type'] !== '')
|
||||
settype($defaultValue, match($spec['type']) {
|
||||
's' => 'string',
|
||||
'a' => 'array',
|
||||
'i' => 'int',
|
||||
'b' => 'bool',
|
||||
'f' => 'float',
|
||||
'd' => 'double',
|
||||
default => throw new InvalidArgumentException(sprintf('Invalid type letter encountered: "%s"', $spec['type'])),
|
||||
});
|
||||
|
||||
$results[$resultName] = $defaultValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
$results[$resultName] = match($spec['type']) {
|
||||
's' => $info->getString(),
|
||||
'a' => $info->getArray(),
|
||||
'i' => $info->getInteger(),
|
||||
'b' => $info->getBoolean(),
|
||||
'f' => $info->getFloat(),
|
||||
'd' => $info->getFloat(),
|
||||
'' => $info->getValue(),
|
||||
default => throw new InvalidArgumentException('Unknown type encountered in $specs.'),
|
||||
};
|
||||
|
||||
unset($info);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
189
src/IConfig.php
Normal file
189
src/IConfig.php
Normal file
|
@ -0,0 +1,189 @@
|
|||
<?php
|
||||
// IConfig.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
/**
|
||||
* Provides a common interface for configuration providers.
|
||||
*/
|
||||
interface IConfig {
|
||||
/**
|
||||
* Creates a scoped configuration instance that prepends a prefix to all names.
|
||||
*
|
||||
* @param non-empty-array<string> ...$prefix Parts of the desired.
|
||||
* @return IConfig A scoped configuration instance.
|
||||
*/
|
||||
public function scopeTo(string ...$prefix): IConfig;
|
||||
|
||||
/**
|
||||
* Gets the separator character used for scoping.
|
||||
*
|
||||
* @return string Separator character.
|
||||
*/
|
||||
public function getSeparator(): string;
|
||||
|
||||
/**
|
||||
* Checks if configurations contains given names.
|
||||
* An empty $names list will always return true.
|
||||
*
|
||||
* @param string|string[] $names Name or names to check for.
|
||||
* @return bool Whether all given names are present or not.
|
||||
*/
|
||||
public function hasValues(string|array $names): bool;
|
||||
|
||||
/**
|
||||
* Removes values with given names from the configuration, if writable.
|
||||
*
|
||||
* @param string|string[] $names Name or names to remove.
|
||||
* @throws \RuntimeException If the configuration is read only.
|
||||
*/
|
||||
public function removeValues(string|array $names): void;
|
||||
|
||||
/**
|
||||
* Lists all value informations from this configuration.
|
||||
*
|
||||
* @param int $range Amount of items to take, 0 for all. Must be a positive integer.
|
||||
* @param int $offset Amount of items to skip. Must be a positive integer. Has no effect if $range is 0.
|
||||
* @throws \InvalidArgumentException If $range or $offset are negative.
|
||||
* @return IConfigValueInfo[] Configuration value infos.
|
||||
*/
|
||||
public function getAllValueInfos(int $range = 0, int $offset = 0): array;
|
||||
|
||||
/**
|
||||
* Gets value informations for one or more items.
|
||||
*
|
||||
* @param string|string[] $names Name or names to retrieve.
|
||||
* @return IConfigValueInfo[] Array with value informations.
|
||||
*/
|
||||
public function getValueInfos(string|array $names): array;
|
||||
|
||||
/**
|
||||
* Gets value information for a single item.
|
||||
*
|
||||
* @param string $name Name or names to retrieve.
|
||||
* @return ?IConfigValueInfo Value information, or null if not present.
|
||||
*/
|
||||
public function getValueInfo(string $name): ?IConfigValueInfo;
|
||||
|
||||
/**
|
||||
* Gets multiple values of varying types at once.
|
||||
*
|
||||
* The format of a $specs entry can be one of the following:
|
||||
* If the entry is a string:
|
||||
* - The name of a value: 'value_name'
|
||||
* - The name of a value followed by a requested type, separated by a colon: 'value_name:s', 'value_name:i', 'value_name:a'
|
||||
* If the entry is an array, all except [0] are optional:
|
||||
* - [0] follows to same format as the above described string
|
||||
* - [1] is the default value to fall back on, MUST be the same type as the one specified in [0].
|
||||
* - [2] is an alternative key for the output array.
|
||||
*
|
||||
* Available types are:
|
||||
* :s - string
|
||||
* :a - array
|
||||
* :i - integer
|
||||
* :b - boolean
|
||||
* :f - float
|
||||
* :d - float
|
||||
*
|
||||
* @param array<string|string[]> $specs Specification of what items to grab.
|
||||
* @throws \InvalidArgumentException If $specs is malformed.
|
||||
* @return array<string, mixed> An associative array containing the retrieved values.
|
||||
*/
|
||||
public function getValues(array $specs): array;
|
||||
|
||||
/**
|
||||
* Gets a single string value.
|
||||
*
|
||||
* @param string $name Name of the value to fetch.
|
||||
* @param string $default Default value to fall back on if the value is not present.
|
||||
* @return string Configuration value for $name.
|
||||
*/
|
||||
public function getString(string $name, string $default = ''): string;
|
||||
|
||||
/**
|
||||
* Gets a single integer value.
|
||||
*
|
||||
* @param string $name Name of the value to fetch.
|
||||
* @param int $default Default value to fall back on if the value is not present.
|
||||
* @return int Configuration value for $name.
|
||||
*/
|
||||
public function getInteger(string $name, int $default = 0): int;
|
||||
|
||||
/**
|
||||
* Gets a single floating point value.
|
||||
*
|
||||
* @param string $name Name of the value to fetch.
|
||||
* @param float $default Default value to fall back on if the value is not present.
|
||||
* @return float Configuration value for $name.
|
||||
*/
|
||||
public function getFloat(string $name, float $default = 0): float;
|
||||
|
||||
/**
|
||||
* Gets a single boolean value.
|
||||
*
|
||||
* @param string $name Name of the value to fetch.
|
||||
* @param bool $default Default value to fall back on if the value is not present.
|
||||
* @return bool Configuration value for $name.
|
||||
*/
|
||||
public function getBoolean(string $name, bool $default = false): bool;
|
||||
|
||||
/**
|
||||
* Gets an array value.
|
||||
*
|
||||
* @param string $name Name of the value to fetch.
|
||||
* @param mixed[] $default Default value to fall back on if the value is not present.
|
||||
* @return mixed[] Configuration value for $name.
|
||||
*/
|
||||
public function getArray(string $name, array $default = []): array;
|
||||
|
||||
/**
|
||||
* Sets multiple values at once using an associative array.
|
||||
*
|
||||
* @param array<string, mixed> $values Values to save.
|
||||
* @throws \InvalidArgumentException If $values is malformed.
|
||||
* @throws \RuntimeException If the configuration is read only.
|
||||
*/
|
||||
public function setValues(array $values): void;
|
||||
|
||||
/**
|
||||
* Sets a single string value.
|
||||
*
|
||||
* @param string $name Name of the value to save.
|
||||
* @param string $value Value to save.
|
||||
*/
|
||||
public function setString(string $name, string $value): void;
|
||||
|
||||
/**
|
||||
* Sets a single integer value.
|
||||
*
|
||||
* @param string $name Name of the value to save.
|
||||
* @param int $value Value to save.
|
||||
*/
|
||||
public function setInteger(string $name, int $value): void;
|
||||
|
||||
/**
|
||||
* Sets a single floating point value.
|
||||
*
|
||||
* @param string $name Name of the value to save.
|
||||
* @param float $value Value to save.
|
||||
*/
|
||||
public function setFloat(string $name, float $value): void;
|
||||
|
||||
/**
|
||||
* Sets a single boolean value.
|
||||
*
|
||||
* @param string $name Name of the value to save.
|
||||
* @param bool $value Value to save.
|
||||
*/
|
||||
public function setBoolean(string $name, bool $value): void;
|
||||
|
||||
/**
|
||||
* Sets an array value.
|
||||
*
|
||||
* @param string $name Name of the value to save.
|
||||
* @param mixed[] $value Value to save.
|
||||
*/
|
||||
public function setArray(string $name, array $value): void;
|
||||
}
|
109
src/IConfigValueInfo.php
Normal file
109
src/IConfigValueInfo.php
Normal file
|
@ -0,0 +1,109 @@
|
|||
<?php
|
||||
// IConfigValueInfo.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Provides a common interface for configuration values.
|
||||
*/
|
||||
interface IConfigValueInfo extends Stringable {
|
||||
/**
|
||||
* Gets the name of this configuration value.
|
||||
*
|
||||
* @return string Configuration value name.
|
||||
*/
|
||||
public function getName(): string;
|
||||
|
||||
/**
|
||||
* Gets the type of this configuration value.
|
||||
*
|
||||
* @return string Configuration value type name.
|
||||
*/
|
||||
public function getType(): string;
|
||||
|
||||
/**
|
||||
* Checks whether the value is a string.
|
||||
*
|
||||
* @return bool True if the value is a string.
|
||||
*/
|
||||
public function isString(): bool;
|
||||
|
||||
/**
|
||||
* Checks whether the value is an integer.
|
||||
*
|
||||
* @return bool True if the value is an integer.
|
||||
*/
|
||||
public function isInteger(): bool;
|
||||
|
||||
/**
|
||||
* Checks whether the value is a floating point number.
|
||||
*
|
||||
* @return bool True if the value is a floating point number.
|
||||
*/
|
||||
public function isFloat(): bool;
|
||||
|
||||
/**
|
||||
* Checks whether the value is a boolean.
|
||||
*
|
||||
* @return bool True if the value is a boolean.
|
||||
*/
|
||||
public function isBoolean(): bool;
|
||||
|
||||
/**
|
||||
* Checks whether the value is an array.
|
||||
*
|
||||
* @return bool True if the value is an array.
|
||||
*/
|
||||
public function isArray(): bool;
|
||||
|
||||
/**
|
||||
* Gets the raw value without any type validation.
|
||||
*
|
||||
* @return mixed The configuration value.
|
||||
*/
|
||||
public function getValue(): mixed;
|
||||
|
||||
/**
|
||||
* Ensures the value is a string and returns the value.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the value is not a string.
|
||||
* @return string String configuration value.
|
||||
*/
|
||||
public function getString(): string;
|
||||
|
||||
/**
|
||||
* Ensures the value is an integer and returns the value.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the value is not an integer.
|
||||
* @return int Integer configuration value.
|
||||
*/
|
||||
public function getInteger(): int;
|
||||
|
||||
/**
|
||||
* Ensures the value is a floating point number and returns the value.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the value is not a floating point number.
|
||||
* @return float Floating point number configuration value.
|
||||
*/
|
||||
public function getFloat(): float;
|
||||
|
||||
/**
|
||||
* Ensures the value is a boolean and returns the value.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the value is not a boolean.
|
||||
* @return bool Boolean configuration value.
|
||||
*/
|
||||
public function getBoolean(): bool;
|
||||
|
||||
/**
|
||||
* Ensures the value is an array and returns the value.
|
||||
*
|
||||
* @throws \UnexpectedValueException If the value is not an array.
|
||||
* @return mixed[] Array configuration value.
|
||||
*/
|
||||
public function getArray(): array;
|
||||
}
|
41
src/ImmutableConfigTrait.php
Normal file
41
src/ImmutableConfigTrait.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
// ImmutableConfigTrait.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Intercepts mutable methods required to be implemented by {@see IConfig} and returns exceptions.
|
||||
*/
|
||||
trait ImmutableConfigTrait {
|
||||
public function removeValues(string|array $names): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setValues(array $values): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setString(string $name, string $value): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setInteger(string $name, int $value): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setFloat(string $name, float $value): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setBoolean(string $name, bool $value): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
|
||||
public function setArray(string $name, array $value): void {
|
||||
throw new RuntimeException('This configuration is read only.');
|
||||
}
|
||||
}
|
31
src/MutableConfigTrait.php
Normal file
31
src/MutableConfigTrait.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
// MutableConfigTrait.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
/**
|
||||
* Defines set aliases so you don't have to.
|
||||
*/
|
||||
trait MutableConfigTrait {
|
||||
public function setString(string $name, string $value): void {
|
||||
$this->setValues([$name => $value]);
|
||||
}
|
||||
|
||||
public function setInteger(string $name, int $value): void {
|
||||
$this->setValues([$name => $value]);
|
||||
}
|
||||
|
||||
public function setFloat(string $name, float $value): void {
|
||||
$this->setValues([$name => $value]);
|
||||
}
|
||||
|
||||
public function setBoolean(string $name, bool $value): void {
|
||||
$this->setValues([$name => $value]);
|
||||
}
|
||||
|
||||
public function setArray(string $name, array $value): void {
|
||||
$this->setValues([$name => $value]);
|
||||
}
|
||||
}
|
67
src/NullConfig.php
Normal file
67
src/NullConfig.php
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
// NullConfig.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
/**
|
||||
* Provides a black hole configuration that will always return the default values.
|
||||
*/
|
||||
class NullConfig implements IConfig {
|
||||
use GetValuesTrait;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function getSeparator(): string {
|
||||
return "\0";
|
||||
}
|
||||
|
||||
public function scopeTo(string ...$prefix): IConfig {
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasValues(string|array $names): bool {
|
||||
return is_array($names) && empty($names);
|
||||
}
|
||||
|
||||
public function getAllValueInfos(int $range = 0, int $offset = 0): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getValueInfos(string|array $names): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getValueInfo(string $name): ?IConfigValueInfo {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getString(string $name, string $default = ''): string {
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getInteger(string $name, int $default = 0): int {
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getFloat(string $name, float $default = 0): float {
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getBoolean(string $name, bool $default = false): bool {
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function getArray(string $name, array $default = []): array {
|
||||
return $default;
|
||||
}
|
||||
|
||||
public function removeValues(string|array $names): void {}
|
||||
public function setValues(array $values): void {}
|
||||
public function setString(string $name, string $value): void {}
|
||||
public function setInteger(string $name, int $value): void {}
|
||||
public function setFloat(string $name, float $value): void {}
|
||||
public function setBoolean(string $name, bool $value): void {}
|
||||
public function setArray(string $name, array $value): void {}
|
||||
}
|
156
src/ScopedConfig.php
Normal file
156
src/ScopedConfig.php
Normal file
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
// ScopedConfig.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Provides a scoped configuration instead.
|
||||
*/
|
||||
class ScopedConfig implements IConfig {
|
||||
private IConfig $config;
|
||||
private string $prefix;
|
||||
/** @var non-empty-array<string> */
|
||||
private array $prefixRaw;
|
||||
private int $prefixLength;
|
||||
|
||||
/** @param string[] $prefixRaw */
|
||||
public function __construct(IConfig $config, array $prefixRaw) {
|
||||
if(empty($prefixRaw))
|
||||
throw new InvalidArgumentException('$prefix may not be empty.');
|
||||
|
||||
$scopeChar = $config->getSeparator();
|
||||
$prefix = implode($scopeChar, $prefixRaw) . $scopeChar;
|
||||
|
||||
$this->config = $config;
|
||||
$this->prefix = $prefix;
|
||||
$this->prefixRaw = $prefixRaw;
|
||||
$this->prefixLength = strlen($prefix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|string[] $names
|
||||
* @return string[]
|
||||
*/
|
||||
private function prefixNames(string|array $names): array {
|
||||
if(is_string($names))
|
||||
return [$this->prefix . $names];
|
||||
|
||||
foreach($names as $key => $name)
|
||||
$names[$key] = $this->prefix . $name;
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
private function prefixName(string $name): string {
|
||||
return $this->prefix . $name;
|
||||
}
|
||||
|
||||
public function scopeTo(string ...$prefix): IConfig {
|
||||
return $this->config->scopeTo(...array_merge($this->prefixRaw, $prefix));
|
||||
}
|
||||
|
||||
public function getSeparator(): string {
|
||||
return $this->config->getSeparator();
|
||||
}
|
||||
|
||||
public function hasValues(string|array $names): bool {
|
||||
return $this->config->hasValues($this->prefixNames($names));
|
||||
}
|
||||
|
||||
public function removeValues(string|array $names): void {
|
||||
$this->config->removeValues($this->prefixNames($names));
|
||||
}
|
||||
|
||||
public function getAllValueInfos(int $range = 0, int $offset = 0): array {
|
||||
$infos = $this->config->getAllValueInfos($range, $offset);
|
||||
foreach($infos as $key => $info)
|
||||
$infos[$key] = new ScopedConfigValueInfo($info, $this->prefixLength);
|
||||
return $infos;
|
||||
}
|
||||
|
||||
public function getValueInfos(string|array $names): array {
|
||||
$infos = $this->config->getValueInfos($this->prefixNames($names));
|
||||
foreach($infos as $key => $info)
|
||||
$infos[$key] = new ScopedConfigValueInfo($info, $this->prefixLength);
|
||||
return $infos;
|
||||
}
|
||||
|
||||
public function getValueInfo(string $name): ?IConfigValueInfo {
|
||||
$info = $this->config->getValueInfo($this->prefixName($name));
|
||||
if($info !== null)
|
||||
$info = new ScopedConfigValueInfo($info, $this->prefixLength);
|
||||
return $info;
|
||||
}
|
||||
|
||||
public function getValues(array $specs): array {
|
||||
foreach($specs as $key => $spec) {
|
||||
if(is_string($spec))
|
||||
$specs[$key] = $this->prefixName($spec);
|
||||
elseif(is_array($spec) && !empty($spec))
|
||||
$specs[$key][0] = $this->prefixName($spec[0]);
|
||||
else
|
||||
throw new InvalidArgumentException('$specs array contains an invalid entry.');
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach($this->config->getValues($specs) as $name => $result)
|
||||
// prefix removal should probably be done with a whitelist of sorts
|
||||
$results[str_starts_with($name, $this->prefix) ? substr($name, $this->prefixLength) : $name] = $result;
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
public function getString(string $name, string $default = ''): string {
|
||||
return $this->config->getString($this->prefixName($name), $default);
|
||||
}
|
||||
|
||||
public function getInteger(string $name, int $default = 0): int {
|
||||
return $this->config->getInteger($this->prefixName($name), $default);
|
||||
}
|
||||
|
||||
public function getFloat(string $name, float $default = 0): float {
|
||||
return $this->config->getFloat($this->prefixName($name), $default);
|
||||
}
|
||||
|
||||
public function getBoolean(string $name, bool $default = false): bool {
|
||||
return $this->config->getBoolean($this->prefixName($name), $default);
|
||||
}
|
||||
|
||||
public function getArray(string $name, array $default = []): array {
|
||||
return $this->config->getArray($this->prefixName($name), $default);
|
||||
}
|
||||
|
||||
public function setValues(array $values): void {
|
||||
if(empty($values))
|
||||
return;
|
||||
|
||||
$prefixed = [];
|
||||
foreach($values as $name => $value)
|
||||
$prefixed[$this->prefixName($name)] = $value;
|
||||
$this->config->setValues($values);
|
||||
}
|
||||
|
||||
public function setString(string $name, string $value): void {
|
||||
$this->config->setString($this->prefixName($name), $value);
|
||||
}
|
||||
|
||||
public function setInteger(string $name, int $value): void {
|
||||
$this->config->setInteger($this->prefixName($name), $value);
|
||||
}
|
||||
|
||||
public function setFloat(string $name, float $value): void {
|
||||
$this->config->setFloat($this->prefixName($name), $value);
|
||||
}
|
||||
|
||||
public function setBoolean(string $name, bool $value): void {
|
||||
$this->config->setBoolean($this->prefixName($name), $value);
|
||||
}
|
||||
|
||||
public function setArray(string $name, array $value): void {
|
||||
$this->config->setArray($this->prefixName($name), $value);
|
||||
}
|
||||
}
|
82
src/ScopedConfigValueInfo.php
Normal file
82
src/ScopedConfigValueInfo.php
Normal file
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
// ScopedConfigValueInfo.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
/**
|
||||
* Provides information about a scoped configuration value.
|
||||
*/
|
||||
class ScopedConfigValueInfo implements IConfigValueInfo {
|
||||
/** @internal */
|
||||
public function __construct(
|
||||
private IConfigValueInfo $info,
|
||||
private int $prefixLength
|
||||
) {}
|
||||
|
||||
public function getName(): string {
|
||||
return substr($this->info->getName(), $this->prefixLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the real name of the configuration value, without removing the prefix.
|
||||
*
|
||||
* @return string Unprefixed configuration value.
|
||||
*/
|
||||
public function getRealName(): string {
|
||||
return $this->info->getName();
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
return $this->info->getType();
|
||||
}
|
||||
|
||||
public function isString(): bool {
|
||||
return $this->info->isString();
|
||||
}
|
||||
|
||||
public function isInteger(): bool {
|
||||
return $this->info->isInteger();
|
||||
}
|
||||
|
||||
public function isFloat(): bool {
|
||||
return $this->info->isFloat();
|
||||
}
|
||||
|
||||
public function isBoolean(): bool {
|
||||
return $this->info->isBoolean();
|
||||
}
|
||||
|
||||
public function isArray(): bool {
|
||||
return $this->info->isArray();
|
||||
}
|
||||
|
||||
public function getValue(): mixed {
|
||||
return $this->info->getValue();
|
||||
}
|
||||
|
||||
public function getString(): string {
|
||||
return $this->info->getString();
|
||||
}
|
||||
|
||||
public function getInteger(): int {
|
||||
return $this->info->getInteger();
|
||||
}
|
||||
|
||||
public function getFloat(): float {
|
||||
return $this->info->getFloat();
|
||||
}
|
||||
|
||||
public function getBoolean(): bool {
|
||||
return $this->info->getBoolean();
|
||||
}
|
||||
|
||||
public function getArray(): array {
|
||||
return $this->info->getArray();
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return (string)$this->info;
|
||||
}
|
||||
}
|
136
src/SharpConfig.php
Normal file
136
src/SharpConfig.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
// SharpConfig.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Index\IO\FileStream;
|
||||
use Index\IO\Stream;
|
||||
|
||||
/**
|
||||
* Provides a configuration in SharpChat format.
|
||||
*/
|
||||
class SharpConfig implements IConfig {
|
||||
use ImmutableConfigTrait, GetValueInfoTrait, GetValuesTrait;
|
||||
|
||||
/**
|
||||
* @param array<string, SharpConfigValueInfo> $values
|
||||
*/
|
||||
public function __construct(private array $values) {}
|
||||
|
||||
public function getSeparator(): string {
|
||||
return ':';
|
||||
}
|
||||
|
||||
public function scopeTo(string ...$prefix): IConfig {
|
||||
return new ScopedConfig($this, $prefix);
|
||||
}
|
||||
|
||||
public function hasValues(string|array $names): bool {
|
||||
if(is_string($names))
|
||||
return array_key_exists($names, $this->values);
|
||||
|
||||
foreach($names as $name)
|
||||
if(!array_key_exists($name, $this->values))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getAllValueInfos(int $range = 0, int $offset = 0): array {
|
||||
if($range === 0)
|
||||
return array_values($this->values);
|
||||
|
||||
if($range < 0)
|
||||
throw new InvalidArgumentException('$range must be a positive integer.');
|
||||
if($offset < 0)
|
||||
throw new InvalidArgumentException('$offset must be greater than zero if a range is specified.');
|
||||
|
||||
return array_slice($this->values, $offset, $range);
|
||||
}
|
||||
|
||||
public function getValueInfos(string|array $names): array {
|
||||
if(is_string($names))
|
||||
return array_key_exists($names, $this->values) ? [$this->values[$names]] : [];
|
||||
|
||||
$infos = [];
|
||||
|
||||
foreach($names as $name)
|
||||
if(array_key_exists($name, $this->values))
|
||||
$infos[] = $this->values[$name];
|
||||
|
||||
return $infos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of SharpConfig from an array of lines.
|
||||
*
|
||||
* @param string[] $lines Config lines.
|
||||
* @return SharpConfig
|
||||
*/
|
||||
public static function fromLines(array $lines): self {
|
||||
$values = [];
|
||||
|
||||
foreach($lines as $line) {
|
||||
$line = trim($line);
|
||||
if($line === '' || $line[0] === '#' || $line[0] === ';')
|
||||
continue;
|
||||
|
||||
$info = new SharpConfigValueInfo(...explode(' ', $line, 2));
|
||||
$values[$info->getName()] = $info;
|
||||
}
|
||||
|
||||
return new SharpConfig($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of SharpConfig from a string.
|
||||
*
|
||||
* @param string $lines Config lines.
|
||||
* @param non-empty-string $newLine Line separator character.
|
||||
* @return SharpConfig
|
||||
*/
|
||||
public static function fromString(string $lines, string $newLine = "\n"): self {
|
||||
return self::fromLines(explode($newLine, $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of SharpConfig from a file.
|
||||
*
|
||||
* @param string $path Config file path.
|
||||
* @throws InvalidArgumentException If $path does not exist.
|
||||
* @return SharpConfig
|
||||
*/
|
||||
public static function fromFile(string $path): self {
|
||||
if(!is_file($path))
|
||||
throw new InvalidArgumentException('$path does not exist.');
|
||||
|
||||
return self::fromStream(FileStream::openRead($path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of SharpConfig from a readable stream.
|
||||
*
|
||||
* @param Stream $stream Config file stream.
|
||||
* @throws InvalidArgumentException If $stream is not readable.
|
||||
* @return SharpConfig
|
||||
*/
|
||||
public static function fromStream(Stream $stream): self {
|
||||
if(!$stream->canRead())
|
||||
throw new InvalidArgumentException('$stream must be readable.');
|
||||
|
||||
$values = [];
|
||||
while(($line = $stream->readLine()) !== null) {
|
||||
$line = trim($line);
|
||||
if($line === '' || $line[0] === '#' || $line[0] === ';')
|
||||
continue;
|
||||
|
||||
$info = new SharpConfigValueInfo(...explode(' ', $line, 2));
|
||||
$values[$info->getName()] = $info;
|
||||
}
|
||||
|
||||
return new SharpConfig($values);
|
||||
}
|
||||
}
|
75
src/SharpConfigValueInfo.php
Normal file
75
src/SharpConfigValueInfo.php
Normal file
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
// SharpConfigValueInfo.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
class SharpConfigValueInfo implements IConfigValueInfo {
|
||||
private string $name;
|
||||
private string $value;
|
||||
|
||||
/** @internal */
|
||||
public function __construct(string $name, string $value = '') {
|
||||
$this->name = $name;
|
||||
$this->value = trim($value);
|
||||
}
|
||||
|
||||
public function getName(): string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType(): string {
|
||||
// SharpChat config format is just all strings and casts on demand
|
||||
return 'string';
|
||||
}
|
||||
|
||||
public function isString(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isInteger(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isFloat(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isBoolean(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function isArray(): bool {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getValue(): mixed {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getString(): string {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function getInteger(): int {
|
||||
return (int)$this->value;
|
||||
}
|
||||
|
||||
public function getFloat(): float {
|
||||
return (float)$this->value;
|
||||
}
|
||||
|
||||
public function getBoolean(): bool {
|
||||
return $this->value !== '0'
|
||||
&& strcasecmp($this->value, 'false') !== 0;
|
||||
}
|
||||
|
||||
public function getArray(): array {
|
||||
return explode(' ', $this->value);
|
||||
}
|
||||
|
||||
public function __toString(): string {
|
||||
return $this->value;
|
||||
}
|
||||
}
|
45
src/SyokuhouInfo.php
Normal file
45
src/SyokuhouInfo.php
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
// SyokuhouInfo.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
namespace Syokuhou;
|
||||
|
||||
use UnexpectedValueException;
|
||||
use Index\Version;
|
||||
|
||||
/**
|
||||
* Retrieves library info.
|
||||
*/
|
||||
final class SyokuhouInfo {
|
||||
private static ?string $versionString = null;
|
||||
private static ?Version $version = null;
|
||||
|
||||
/**
|
||||
* Returns the current version of the library.
|
||||
*
|
||||
* @return Version
|
||||
*/
|
||||
public static function getVersion(): Version {
|
||||
if(self::$version === null)
|
||||
self::$version = Version::parse(self::getVersionString());
|
||||
|
||||
return self::$version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current version of the library as a string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function getVersionString(): string {
|
||||
if(self::$versionString === null) {
|
||||
$body = file_get_contents(__DIR__ . '/../VERSION');
|
||||
if($body === false)
|
||||
throw new UnexpectedValueException('Was unable to read VERSION file.');
|
||||
self::$versionString = trim($body);
|
||||
}
|
||||
|
||||
return self::$versionString;
|
||||
}
|
||||
}
|
147
tests/DbConfigTest.php
Normal file
147
tests/DbConfigTest.php
Normal file
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
// DbConfigTest.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Syokuhou\DbConfigTest
|
||||
* @covers \Syokuhou\DbConfigValueInfo
|
||||
* @covers \Syokuhou\MutableConfigTrait
|
||||
* @covers \Syokuhou\GetValueInfoTrait
|
||||
* @covers \Syokuhou\GetValuesTrait
|
||||
*/
|
||||
final class DbConfigTest extends TestCase {
|
||||
private \Syokuhou\DbConfig $config;
|
||||
|
||||
private const VALUES = [
|
||||
'private.allow_password_reset' => 'b:1;',
|
||||
'private.enable' => 'b:0;',
|
||||
'private.msg' => 's:71:"Things are happening. Check back later for something new... eventually.";',
|
||||
'private.perm.cat' => 's:4:"user";',
|
||||
'private.perm.val' => 'i:1;',
|
||||
'site.desc' => 's:38:"The internet\'s last convenience store.";',
|
||||
'site.ext_logo' => 's:51:"https://static.flash.moe/images/flashii-logo-v3.png";',
|
||||
'site.name' => 's:5:"Edgii";',
|
||||
'site.social.bsky' => 's:36:"https://bsky.app/profile/flashii.net";',
|
||||
'site.url' => 's:18:"https://edgii.net/";',
|
||||
'test.array' => 'a:5:{i:0;i:1234;i:1;d:56.789;i:2;s:6:"Mewow!";i:3;b:1;i:4;s:4:"jeff";}',
|
||||
'test.bool' => 'b:1;',
|
||||
'test.float' => 'd:9876.4321;',
|
||||
'test.int' => 'i:243230;',
|
||||
];
|
||||
|
||||
protected function setUp(): void {
|
||||
$dbConn = \Index\Data\DbTools::create('sqlite::memory:');
|
||||
$dbConn->execute('CREATE TABLE skh_config (config_name TEXT NOT NULL COLLATE NOCASE, config_value BLOB NOT NULL, PRIMARY KEY (config_name))');
|
||||
|
||||
$stmt = $dbConn->prepare('INSERT INTO skh_config (config_name, config_value) VALUES (?, ?)');
|
||||
foreach(self::VALUES as $name => $value) {
|
||||
$stmt->addParameter(1, $name);
|
||||
$stmt->addParameter(2, $value);
|
||||
$stmt->execute();
|
||||
}
|
||||
|
||||
$this->config = new \Syokuhou\DbConfig($dbConn, 'skh_config');
|
||||
}
|
||||
|
||||
public function testScoping(): void {
|
||||
$this->assertEquals('user', $this->config->getString('private.perm.cat'));
|
||||
$this->assertEquals('Edgii', $this->config->getString('site.name'));
|
||||
|
||||
$scoped = $this->config->scopeTo('private', 'perm');
|
||||
$this->assertEquals('user', $scoped->getString('cat'));
|
||||
}
|
||||
|
||||
public function testHasValues(): void {
|
||||
// hasValues should always return true when the list is empty
|
||||
$this->assertTrue($this->config->hasValues([]));
|
||||
$this->assertFalse($this->config->hasValues('meow'));
|
||||
$this->assertTrue($this->config->hasValues('site.desc'));
|
||||
$this->assertTrue($this->config->hasValues(['site.ext_logo', 'site.url']));
|
||||
$this->assertFalse($this->config->hasValues(['site.ext_logo', 'site.url', 'site.gun']));
|
||||
}
|
||||
|
||||
public function testGetAllValueInfos(): void {
|
||||
$all = $this->config->getAllValueInfos();
|
||||
$expected = array_keys(self::VALUES);
|
||||
$values = [];
|
||||
|
||||
foreach($all as $info)
|
||||
$values[] = $info->getName();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$subset = $this->config->getAllValueInfos(2, 3);
|
||||
$expected = [
|
||||
'private.perm.cat',
|
||||
'private.perm.val',
|
||||
];
|
||||
$values = [];
|
||||
|
||||
foreach($subset as $info)
|
||||
$values[] = $info->getName();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
}
|
||||
|
||||
public function testGetValues(): void {
|
||||
$this->assertNull($this->config->getValueInfo('doesnotexist'));
|
||||
|
||||
$scoped = $this->config->scopeTo('private', 'perm');
|
||||
|
||||
$expected = ['private.perm.cat' => 'user', 'private.perm.val' => 1];
|
||||
$values = [];
|
||||
$valueInfos = $scoped->getValueInfos(['cat', 'val', 'poop']);
|
||||
foreach($valueInfos as $valueInfo)
|
||||
$values[$valueInfo->getRealName()] = $valueInfo->getValue();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$scoped = $this->config->scopeTo('site')->scopeTo('social');
|
||||
|
||||
$expected = [
|
||||
'bsky' => 'https://bsky.app/profile/flashii.net',
|
||||
'bsky_show' => true,
|
||||
'twitter' => '',
|
||||
'twitterShow' => false,
|
||||
];
|
||||
$values = $scoped->getValues([
|
||||
'bsky',
|
||||
['bsky_show:b', true],
|
||||
'twitter:s',
|
||||
['twitter_show:b', false, 'twitterShow'],
|
||||
]);
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$this->assertEquals('', $this->config->getString('none.string'));
|
||||
$this->assertEquals('test', $this->config->getString('none.string', 'test'));
|
||||
$this->assertEquals('https://edgii.net/', $this->config->getString('site.url'));
|
||||
$this->assertEquals(0, $this->config->getInteger('site.url'));
|
||||
|
||||
$this->assertEquals(0, $this->config->getInteger('none.int'));
|
||||
$this->assertEquals(10, $this->config->getInteger('none.int', 10));
|
||||
$this->assertEquals(243230, $this->config->getInteger('test.int'));
|
||||
$this->assertEquals('', $this->config->getString('test.int'));
|
||||
|
||||
$this->assertEquals(0, $this->config->getFloat('none.float'));
|
||||
$this->assertEquals(0.1, $this->config->getFloat('none.float', 0.1));
|
||||
$this->assertEquals(9876.4321, $this->config->getFloat('test.float'));
|
||||
$this->assertEmpty($this->config->getArray('test.float'));
|
||||
|
||||
$this->assertEquals(false, $this->config->getBoolean('none.bool'));
|
||||
$this->assertEquals(true, $this->config->getBoolean('none.bool', true));
|
||||
$this->assertEquals(true, $this->config->getBoolean('test.bool'));
|
||||
$this->assertEquals(false, $this->config->getBoolean('private.msg'));
|
||||
$this->assertEquals(0, $this->config->getFloat('test.bool'));
|
||||
|
||||
$this->assertEmpty($this->config->getArray('none.array'));
|
||||
$this->assertEquals(['de', 'het', 'een'], $this->config->getArray('none.array', ['de', 'het', 'een']));
|
||||
$this->assertEquals([1234, 56.789, 'Mewow!', true, 'jeff'], $this->config->getArray('test.array'));
|
||||
$this->assertEquals(false, $this->config->getBoolean('test.array'));
|
||||
}
|
||||
}
|
80
tests/NullConfigTest.php
Normal file
80
tests/NullConfigTest.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
// NullConfigTest.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Syokuhou\NullConfig
|
||||
* @covers \Syokuhou\GetValuesTrait
|
||||
*/
|
||||
final class NullConfigTest extends TestCase {
|
||||
public function testNullConfig(): void {
|
||||
$config = new \Syokuhou\NullConfig;
|
||||
|
||||
// no-ops but run anyway to ensure no screaming
|
||||
$config->removeValues('test');
|
||||
$config->setString('stringval', 'the');
|
||||
$config->setInteger('intval', 1234);
|
||||
$config->setFloat('floatval', 56.78);
|
||||
$config->setBoolean('boolval', true);
|
||||
$config->setArray('arrval', ['meow']);
|
||||
$config->setValues([
|
||||
'stringval' => 'the',
|
||||
'intval' => 1234,
|
||||
'floatval' => 56.78,
|
||||
'boolval' => true,
|
||||
'arrval' => ['meow'],
|
||||
]);
|
||||
|
||||
// NullConfig currently returns itself when scoping
|
||||
// might change this depending on whether the scope prefix will be exposed or not
|
||||
$scoped = $config->scopeTo('scoped');
|
||||
$this->assertEquals($config, $scoped);
|
||||
|
||||
// hasValues should always return true when the list is empty
|
||||
$this->assertTrue($config->hasValues([]));
|
||||
$this->assertFalse($config->hasValues('anything'));
|
||||
$this->assertFalse($config->hasValues(['manything1', 'manything2']));
|
||||
|
||||
$this->assertEmpty($config->getAllValueInfos());
|
||||
$this->assertEmpty($config->getValueInfos('the'));
|
||||
|
||||
$expected = [
|
||||
'test_no_type' => null,
|
||||
'test_yes_type' => false,
|
||||
'test_no_type_yes_default' => 1234,
|
||||
'test_yes_type_yes_default' => 56.78,
|
||||
'aliased' => null,
|
||||
];
|
||||
$values = $config->getValues([
|
||||
'test_no_type',
|
||||
'test_yes_type:b',
|
||||
['test_no_type_yes_default', 1234],
|
||||
['test_yes_type_yes_default:d', 56.78],
|
||||
['test_no_default_yes_alias', null, 'aliased'],
|
||||
]);
|
||||
|
||||
$this->assertEqualsCanonicalizing($expected, $values);
|
||||
|
||||
$this->assertNull($config->getValueInfo('value'));
|
||||
|
||||
$this->assertEquals('', $config->getString('string'));
|
||||
$this->assertEquals('default', $config->getString('string', 'default'));
|
||||
|
||||
$this->assertEquals(0, $config->getInteger('int'));
|
||||
$this->assertEquals(960, $config->getInteger('int', 960));
|
||||
|
||||
$this->assertEquals(0, $config->getFloat('float'));
|
||||
$this->assertEquals(67.7, $config->getFloat('float', 67.7));
|
||||
|
||||
$this->assertFalse($config->getBoolean('bool'));
|
||||
$this->assertTrue($config->getBoolean('bool', true));
|
||||
|
||||
$this->assertEmpty($config->getArray('arr'));
|
||||
$this->assertEqualsCanonicalizing(['de', 'het', 'een'], $config->getArray('the', ['de', 'het', 'een']));
|
||||
}
|
||||
}
|
190
tests/SharpConfigTest.php
Normal file
190
tests/SharpConfigTest.php
Normal file
|
@ -0,0 +1,190 @@
|
|||
<?php
|
||||
// SharpConfigTest.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Syokuhou\SharpConfig
|
||||
* @covers \Syokuhou\SharpConfigValueInfo
|
||||
* @covers \Syokuhou\ImmutableConfigTrait
|
||||
* @covers \Syokuhou\GetValueInfoTrait
|
||||
* @covers \Syokuhou\GetValuesTrait
|
||||
*/
|
||||
final class SharpConfigTest extends TestCase {
|
||||
public function testImmutableRemove(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test value')->removeValues('test');
|
||||
}
|
||||
|
||||
public function testImmutableSetString(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test value')->setString('test', 'the');
|
||||
}
|
||||
|
||||
public function testImmutableSetInteger(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test 1234')->setInteger('test', 5678);
|
||||
}
|
||||
|
||||
public function testImmutableSetFloat(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test 56.78')->setFloat('test', 12.34);
|
||||
}
|
||||
|
||||
public function testImmutableSetBoolean(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test true')->setBoolean('test', false);
|
||||
}
|
||||
|
||||
public function testImmutableSetArray(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('test words words words')->setArray('test', ['meow', 'meow', 'meow']);
|
||||
}
|
||||
|
||||
public function testImmutableSetValues(): void {
|
||||
$this->expectException(\RuntimeException::class);
|
||||
\Syokuhou\SharpConfig::fromString('')->setValues([
|
||||
'stringval' => 'the',
|
||||
'intval' => 1234,
|
||||
'floatval' => 56.78,
|
||||
'boolval' => true,
|
||||
'arrval' => ['meow'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function testScoping(): void {
|
||||
$config = \Syokuhou\SharpConfig::fromLines([
|
||||
'test Inaccessible',
|
||||
'scoped:test Accessible',
|
||||
]);
|
||||
|
||||
$this->assertEquals('Inaccessible', $config->getString('test'));
|
||||
$this->assertEquals('Accessible', $config->getString('scoped:test'));
|
||||
|
||||
$scoped = $config->scopeTo('scoped');
|
||||
$this->assertEquals('Accessible', $scoped->getString('test'));
|
||||
}
|
||||
|
||||
public function testHasValues(): void {
|
||||
$config = \Syokuhou\SharpConfig::fromLines([
|
||||
'test 123',
|
||||
'scoped:test true',
|
||||
'scoped:meow meow',
|
||||
]);
|
||||
|
||||
// hasValues should always return true when the list is empty
|
||||
$this->assertTrue($config->hasValues([]));
|
||||
$this->assertFalse($config->hasValues('meow'));
|
||||
$this->assertTrue($config->hasValues('test'));
|
||||
$this->assertFalse($config->hasValues(['test', 'meow']));
|
||||
$this->assertTrue($config->hasValues(['scoped:test', 'scoped:meow']));
|
||||
}
|
||||
|
||||
public function testGetAllValueInfos(): void {
|
||||
$config = \Syokuhou\SharpConfig::fromFile(__DIR__ . '/sharpchat.cfg');
|
||||
|
||||
$all = $config->getAllValueInfos();
|
||||
$expected = [
|
||||
'chat:port',
|
||||
'chat:msgMaxLength',
|
||||
'chat:floodKickLength',
|
||||
'chat:channels',
|
||||
'chat:channels:lounge:name',
|
||||
'chat:channels:lounge:autoJoin',
|
||||
'chat:channels:prog:name',
|
||||
'chat:channels:games:name',
|
||||
'chat:channels:splat:name',
|
||||
'chat:channels:passwd:name',
|
||||
'chat:channels:passwd:password',
|
||||
'chat:channels:staff:name',
|
||||
'chat:channels:staff:minRank',
|
||||
'msz:secret',
|
||||
'msz:url',
|
||||
'mariadb:host',
|
||||
'mariadb:user',
|
||||
'mariadb:pass',
|
||||
'mariadb:db',
|
||||
];
|
||||
$values = [];
|
||||
|
||||
foreach($all as $info)
|
||||
$values[] = $info->getName();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$subset = $config->getAllValueInfos(3, 6);
|
||||
$expected = [
|
||||
'chat:channels:prog:name',
|
||||
'chat:channels:games:name',
|
||||
'chat:channels:splat:name',
|
||||
];
|
||||
$values = [];
|
||||
|
||||
foreach($subset as $info)
|
||||
$values[] = $info->getName();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
}
|
||||
|
||||
public function testGetValues(): void {
|
||||
$config = \Syokuhou\SharpConfig::fromFile(__DIR__ . '/sharpchat.cfg');
|
||||
|
||||
$this->assertNull($config->getValueInfo('doesnotexist'));
|
||||
|
||||
$scoped = $config->scopeTo('chat')->scopeTo('channels', 'passwd');
|
||||
|
||||
$expected = ['chat:channels:passwd:name' => 'Password', 'chat:channels:passwd:password' => 'meow'];
|
||||
$values = [];
|
||||
$valueInfos = $scoped->getValueInfos(['name', 'password', 'minRank']);
|
||||
foreach($valueInfos as $valueInfo)
|
||||
$values[$valueInfo->getRealName()] = $valueInfo->getValue();
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$scoped = $config->scopeTo('chat', 'channels', 'lounge');
|
||||
|
||||
$expected = [
|
||||
'name' => 'Lounge',
|
||||
'auto_join' => true,
|
||||
'minRank' => 0,
|
||||
];
|
||||
$values = $scoped->getValues([
|
||||
'name',
|
||||
['autoJoin:b', false, 'auto_join'],
|
||||
'minRank:i',
|
||||
]);
|
||||
|
||||
$this->assertEquals($expected, $values);
|
||||
|
||||
$this->assertEquals('', $config->getString('msz:url2'));
|
||||
$this->assertEquals('test', $config->getString('msz:url2', 'test'));
|
||||
$this->assertEquals('https://flashii.net/_sockchat', $config->getString('msz:url'));
|
||||
|
||||
$this->assertEquals(0, $config->getInteger('chat:connMaxCount'));
|
||||
$this->assertEquals(10, $config->getInteger('chat:connMaxCount', 10));
|
||||
$this->assertEquals(30, $config->getInteger('chat:floodKickLength'));
|
||||
$this->assertEquals('30', $config->getString('chat:floodKickLength'));
|
||||
|
||||
$this->assertEquals(0, $config->getFloat('boat'));
|
||||
$this->assertEquals(0.1, $config->getFloat('boat', 0.1));
|
||||
$this->assertEquals(192.168, $config->getFloat('mariadb:host'));
|
||||
$this->assertEquals('192.168.0.123', $config->getString('mariadb:host'));
|
||||
|
||||
$this->assertEquals(false, $config->getBoolean('nonexist'));
|
||||
$this->assertEquals(true, $config->getBoolean('nonexist', true));
|
||||
$this->assertEquals(true, $config->getBoolean('chat:channels:lounge:autoJoin'));
|
||||
$this->assertEquals('true', $config->getString('chat:channels:lounge:autoJoin'));
|
||||
$this->assertEquals(true, $config->getBoolean('mariadb:db'));
|
||||
|
||||
$this->assertEmpty($config->getArray('nonexist'));
|
||||
$this->assertEquals(['de', 'het', 'een'], $config->getArray('nonexist', ['de', 'het', 'een']));
|
||||
$this->assertEquals(['lounge', 'prog', 'games', 'splat', 'passwd', 'staff'], $config->getArray('chat:channels'));
|
||||
$this->assertEquals('lounge prog games splat passwd staff', $config->getString('chat:channels'));
|
||||
$this->assertEquals(['fake', 'secret', 'meow'], $config->getArray('msz:secret'));
|
||||
$this->assertEquals('fake secret meow', $config->getString('msz:secret'));
|
||||
}
|
||||
}
|
19
tests/SyokuhouTest.php
Normal file
19
tests/SyokuhouTest.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
// SyokuhouTest.php
|
||||
// Created: 2023-10-20
|
||||
// Updated: 2023-10-20
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \Syokuhou\SyokuhouInfo
|
||||
*/
|
||||
final class SyokuhouTest extends TestCase {
|
||||
public function testVersionDecode(): void {
|
||||
$expected = trim(file_get_contents(__DIR__ . '/../VERSION'));
|
||||
$this->assertEquals($expected, \Syokuhou\SyokuhouInfo::getVersionString());
|
||||
$this->assertEquals($expected, (string)\Syokuhou\SyokuhouInfo::getVersion());
|
||||
}
|
||||
}
|
36
tests/sharpchat.cfg
Normal file
36
tests/sharpchat.cfg
Normal file
|
@ -0,0 +1,36 @@
|
|||
# and ; can be used at the start of a line for comments.
|
||||
|
||||
# General Configuration
|
||||
chat:port 6770
|
||||
chat:msgMaxLength 5000
|
||||
#chat:connMaxCount 5
|
||||
chat:floodKickLength 30
|
||||
|
||||
# Channels
|
||||
chat:channels lounge prog games splat passwd staff
|
||||
|
||||
# Lounge channel settings
|
||||
chat:channels:lounge:name Lounge
|
||||
chat:channels:lounge:autoJoin true
|
||||
|
||||
chat:channels:prog:name Programming
|
||||
chat:channels:games:name Games
|
||||
chat:channels:splat:name Splatoon
|
||||
|
||||
# Passworded channel
|
||||
chat:channels:passwd:name Password
|
||||
chat:channels:passwd:password meow
|
||||
|
||||
# Staff channel settings
|
||||
chat:channels:staff:name Staff
|
||||
chat:channels:staff:minRank 5
|
||||
|
||||
# Misuzu integration settings
|
||||
msz:secret fake secret meow
|
||||
msz:url https://flashii.net/_sockchat
|
||||
|
||||
# MariaDB configuration
|
||||
mariadb:host 192.168.0.123
|
||||
mariadb:user chat
|
||||
mariadb:pass nyaa
|
||||
mariadb:db chat
|
8
tools/precommit.sh
Executable file
8
tools/precommit.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/bash
|
||||
|
||||
pushd .
|
||||
cd $(dirname "$0")
|
||||
|
||||
php update-headers.php
|
||||
|
||||
popd
|
172
tools/update-headers.php
Normal file
172
tools/update-headers.php
Normal file
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
// the point of index was so that i wouldn't have to copy things between projects
|
||||
// here i am copying things from index
|
||||
|
||||
date_default_timezone_set('utc');
|
||||
|
||||
function git_changes(): array {
|
||||
$files = [];
|
||||
|
||||
$dir = getcwd();
|
||||
try {
|
||||
chdir(__DIR__ . '/..');
|
||||
|
||||
$output = explode("\n", trim(shell_exec('git status --short --porcelain=v1 --untracked-files=all --no-column')));
|
||||
|
||||
foreach($output as $line) {
|
||||
$line = trim($line);
|
||||
$file = realpath(explode(' ', $line)[1]);
|
||||
$files[] = $file;
|
||||
}
|
||||
|
||||
} finally {
|
||||
chdir($dir);
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
function collect_files(string $directory): array {
|
||||
$files = [];
|
||||
$dir = glob(realpath($directory) . DIRECTORY_SEPARATOR . '*');
|
||||
|
||||
foreach($dir as $file) {
|
||||
if(is_dir($file)) {
|
||||
$files = array_merge($files, collect_files($file));
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[] = $file;
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
echo 'Indexing changed files according to git...' . PHP_EOL;
|
||||
|
||||
$changed = git_changes();
|
||||
|
||||
echo 'Collecting files...' . PHP_EOL;
|
||||
|
||||
$sources = collect_files(__DIR__ . '/../src');
|
||||
$tests = collect_files(__DIR__ . '/../tests');
|
||||
|
||||
$files = array_merge($sources, $tests);
|
||||
|
||||
$topDir = dirname(__DIR__) . DIRECTORY_SEPARATOR;
|
||||
|
||||
$now = date('Y-m-d');
|
||||
|
||||
foreach($files as $file) {
|
||||
echo 'Scanning ' . str_replace($topDir, '', $file) . '...' . PHP_EOL;
|
||||
|
||||
try {
|
||||
$handle = fopen($file, 'rb');
|
||||
|
||||
$checkPHP = trim(fgets($handle)) === '<?php';
|
||||
if(!$checkPHP) {
|
||||
echo 'File is not PHP.' . PHP_EOL;
|
||||
continue;
|
||||
}
|
||||
|
||||
$headerLines = [];
|
||||
|
||||
$expectLine = '// ' . basename($file);
|
||||
$nameLine = trim(fgets($handle));
|
||||
if($nameLine !== $expectLine) {
|
||||
echo ' File name is missing or invalid, queuing update...' . PHP_EOL;
|
||||
$headerLines['name'] = $expectLine;
|
||||
}
|
||||
|
||||
$createdPrefix = '// Created: ';
|
||||
$createdLine = trim(fgets($handle));
|
||||
if(strpos($createdLine, $createdPrefix) !== 0) {
|
||||
echo ' Creation date is missing, queuing update...' . PHP_EOL;
|
||||
$headerLines['created'] = $createdPrefix . $now;
|
||||
}
|
||||
|
||||
$updatedPrefix = '// Updated: ';
|
||||
$updatedLine = trim(fgets($handle));
|
||||
$updatedDate = substr($updatedLine, strlen($updatedPrefix));
|
||||
if(strpos($updatedLine, $updatedPrefix) !== 0 || (in_array($file, $changed) && $updatedDate !== $now)) {
|
||||
echo ' Updated date is inaccurate, queuing update...' . PHP_EOL;
|
||||
$headerLines['updated'] = $updatedPrefix . $now;
|
||||
}
|
||||
|
||||
$blankLine = trim(fgets($handle));
|
||||
if(!empty($blankLine)) {
|
||||
echo ' Trailing newline missing, queuing update...' . PHP_EOL;
|
||||
$headerLines['blank'] = '';
|
||||
}
|
||||
|
||||
if(!empty($headerLines)) {
|
||||
fclose($handle);
|
||||
|
||||
try {
|
||||
$tmpName = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ndx-uh-' . bin2hex(random_bytes(8)) . '.tmp';
|
||||
copy($file, $tmpName);
|
||||
|
||||
$read = fopen($tmpName, 'rb');
|
||||
$handle = fopen($file, 'wb');
|
||||
|
||||
fwrite($handle, fgets($read));
|
||||
|
||||
$insertAfter = [];
|
||||
|
||||
if(empty($headerLines['name']))
|
||||
fwrite($handle, fgets($read));
|
||||
else {
|
||||
$line = fgets($read);
|
||||
if(strpos($line, '// ') !== 0)
|
||||
$insertAfter[] = $line;
|
||||
|
||||
fwrite($handle, $headerLines['name'] . "\n");
|
||||
}
|
||||
|
||||
if(empty($headerLines['created']))
|
||||
fwrite($handle, fgets($read));
|
||||
else {
|
||||
$line = fgets($read);
|
||||
if(strpos($line, '// Created: ') !== 0)
|
||||
$insertAfter[] = $line;
|
||||
|
||||
fwrite($handle, $headerLines['created'] . "\n");
|
||||
}
|
||||
|
||||
if(empty($headerLines['updated']))
|
||||
fwrite($handle, fgets($read));
|
||||
else {
|
||||
$line = fgets($read);
|
||||
if(strpos($line, '// Updated: ') !== 0)
|
||||
$insertAfter[] = $line;
|
||||
|
||||
fwrite($handle, $headerLines['updated'] . "\n");
|
||||
}
|
||||
|
||||
if(!isset($headerLines['blank']))
|
||||
fwrite($handle, fgets($read));
|
||||
else {
|
||||
$line = fgets($read);
|
||||
if(!empty($line)) {
|
||||
$insertAfter[] = $line;
|
||||
fwrite($handle, "\n");
|
||||
}
|
||||
}
|
||||
|
||||
foreach($insertAfter as $line)
|
||||
fwrite($handle, $line);
|
||||
|
||||
while(($line = fgets($read)) !== false)
|
||||
fwrite($handle, $line);
|
||||
} finally {
|
||||
if(is_resource($read))
|
||||
fclose($read);
|
||||
|
||||
if(is_file($tmpName))
|
||||
unlink($tmpName);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
Reference in a new issue