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