mining checker
This commit is contained in:
@@ -11,6 +11,7 @@ use Desktop\DesktopState;
|
||||
use Desktop\SkinResolver;
|
||||
use Desktop\WidgetRegistry;
|
||||
use Desktop\WindowManager;
|
||||
use ModulesCore\ModuleRegistry;
|
||||
|
||||
final class App
|
||||
{
|
||||
@@ -26,7 +27,8 @@ final class App
|
||||
{
|
||||
$keycloakConfig = ConfigLoader::load($this->projectRoot, 'keycloak');
|
||||
$auth = new KeycloakAuth($keycloakConfig);
|
||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php');
|
||||
$moduleRegistry = new ModuleRegistry($this->projectRoot . '/modules');
|
||||
$registry = new AppRegistry($this->projectRoot . '/config/apps.php', $moduleRegistry->desktopApps());
|
||||
$widgetRegistry = new WidgetRegistry($this->projectRoot . '/config/widgets.php');
|
||||
$skins = SkinResolver::all();
|
||||
$isAuthenticated = isset($_SESSION['desktop_auth']) && is_array($_SESSION['desktop_auth']);
|
||||
|
||||
@@ -6,7 +6,7 @@ spl_autoload_register(static function (string $class): void {
|
||||
$prefixes = [
|
||||
'App\\' => __DIR__ . '/',
|
||||
'Desktop\\' => dirname(__DIR__) . '/Desktop/',
|
||||
'MiningChecker\\' => dirname(__DIR__) . '/MiningChecker/',
|
||||
'ModulesCore\\' => dirname(__DIR__) . '/ModulesCore/',
|
||||
];
|
||||
|
||||
foreach ($prefixes as $prefix => $baseDir) {
|
||||
|
||||
@@ -9,11 +9,14 @@ final class AppRegistry
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
private array $apps;
|
||||
|
||||
public function __construct(string $configPath)
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $additionalApps
|
||||
*/
|
||||
public function __construct(string $configPath, array $additionalApps = [])
|
||||
{
|
||||
/** @var array<int, array<string, mixed>> $apps */
|
||||
$apps = require $configPath;
|
||||
$this->apps = $apps;
|
||||
$this->apps = array_values(array_merge($apps, $additionalApps));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class LegacyModuleStore
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
private readonly string $userScope,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function loadProject(string $projectKey): array
|
||||
{
|
||||
$state = $this->loadAll();
|
||||
$projects = is_array($state['projects'] ?? null) ? $state['projects'] : [];
|
||||
|
||||
if (!isset($projects[$projectKey]) || !is_array($projects[$projectKey])) {
|
||||
$projects[$projectKey] = $this->defaultProjectState($projectKey);
|
||||
$state['projects'] = $projects;
|
||||
$this->saveAll($state);
|
||||
}
|
||||
|
||||
return array_replace_recursive($this->defaultProjectState($projectKey), $projects[$projectKey]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $projectState
|
||||
*/
|
||||
public function saveProject(string $projectKey, array $projectState): void
|
||||
{
|
||||
$state = $this->loadAll();
|
||||
$projects = is_array($state['projects'] ?? null) ? $state['projects'] : [];
|
||||
$projects[$projectKey] = array_replace_recursive($this->defaultProjectState($projectKey), $projectState);
|
||||
$state['projects'] = $projects;
|
||||
$this->saveAll($state);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function loadAuth(): array
|
||||
{
|
||||
$state = $this->loadAll();
|
||||
$auth = is_array($state['module_auth'] ?? null) ? $state['module_auth'] : [];
|
||||
|
||||
return array_replace_recursive([
|
||||
'required' => true,
|
||||
'users' => [],
|
||||
'groups' => [],
|
||||
], $auth);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $auth
|
||||
*/
|
||||
public function saveAuth(array $auth): void
|
||||
{
|
||||
$state = $this->loadAll();
|
||||
$state['module_auth'] = [
|
||||
'required' => (bool) ($auth['required'] ?? true),
|
||||
'users' => array_values(array_filter(array_map('strval', (array) ($auth['users'] ?? [])))),
|
||||
'groups' => array_values(array_filter(array_map('strval', (array) ($auth['groups'] ?? [])))),
|
||||
];
|
||||
$this->saveAll($state);
|
||||
}
|
||||
|
||||
public function nextId(string $projectKey, string $bucket): int
|
||||
{
|
||||
$project = $this->loadProject($projectKey);
|
||||
$counters = is_array($project['_counters'] ?? null) ? $project['_counters'] : [];
|
||||
$next = (int) ($counters[$bucket] ?? 0) + 1;
|
||||
$counters[$bucket] = $next;
|
||||
$project['_counters'] = $counters;
|
||||
$this->saveProject($projectKey, $project);
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function loadAll(): array
|
||||
{
|
||||
$path = $this->path();
|
||||
|
||||
if (!is_file($path)) {
|
||||
return [
|
||||
'module_auth' => [
|
||||
'required' => true,
|
||||
'users' => [],
|
||||
'groups' => [],
|
||||
],
|
||||
'projects' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
return [
|
||||
'module_auth' => [
|
||||
'required' => true,
|
||||
'users' => [],
|
||||
'groups' => [],
|
||||
],
|
||||
'projects' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
return is_array($data) ? $data : [
|
||||
'module_auth' => [
|
||||
'required' => true,
|
||||
'users' => [],
|
||||
'groups' => [],
|
||||
],
|
||||
'projects' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $state
|
||||
*/
|
||||
private function saveAll(array $state): void
|
||||
{
|
||||
$directory = dirname($this->path());
|
||||
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('Mining-Checker Speicherverzeichnis konnte nicht erstellt werden.');
|
||||
}
|
||||
|
||||
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
throw new RuntimeException('Mining-Checker Speicherinhalt konnte nicht serialisiert werden.');
|
||||
}
|
||||
|
||||
if (file_put_contents($this->path(), $json . PHP_EOL, LOCK_EX) === false) {
|
||||
throw new RuntimeException('Mining-Checker Speicherinhalt konnte nicht geschrieben werden.');
|
||||
}
|
||||
}
|
||||
|
||||
private function path(): string
|
||||
{
|
||||
$safeScope = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $this->userScope) ?: 'guest';
|
||||
|
||||
return $this->projectRoot . '/data/mining-checker/users/' . $safeScope . '.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultProjectState(string $projectKey): array
|
||||
{
|
||||
return [
|
||||
'_counters' => [],
|
||||
'project' => [
|
||||
'project_key' => $projectKey,
|
||||
'project_name' => strtoupper(str_replace('-', ' ', $projectKey)),
|
||||
],
|
||||
'settings' => [
|
||||
'baseline_measured_at' => gmdate('Y-m-d H:i:s'),
|
||||
'baseline_coins_total' => 0,
|
||||
'daily_cost_amount' => 0,
|
||||
'daily_cost_currency' => 'EUR',
|
||||
'report_currency' => 'EUR',
|
||||
'crypto_currency' => 'DOGE',
|
||||
'display_timezone' => 'Europe/Berlin',
|
||||
'fx_max_age_hours' => 3,
|
||||
'module_theme_mode' => 'inherit',
|
||||
'module_theme_accent' => 'teal',
|
||||
'preferred_currencies' => ['DOGE', 'USD', 'EUR'],
|
||||
],
|
||||
'measurements' => [],
|
||||
'targets' => [],
|
||||
'dashboards' => [],
|
||||
'wallet_snapshots' => [],
|
||||
'cost_plans' => [],
|
||||
'payouts' => [],
|
||||
'miner_offers' => [],
|
||||
'purchased_miners' => [],
|
||||
'fx_history' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
final class MiningCheckerCalculator
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array<string, float|null>
|
||||
*/
|
||||
public function calculate(array $settings): array
|
||||
{
|
||||
$hashrate = $this->toHashrateHps(
|
||||
(float) ($settings['hashrate'] ?? 0),
|
||||
(string) ($settings['hashrate_unit'] ?? 'MH/s')
|
||||
);
|
||||
$networkHashrate = $this->toHashrateHps(
|
||||
(float) ($settings['network_hashrate'] ?? 0),
|
||||
(string) ($settings['network_hashrate_unit'] ?? 'TH/s')
|
||||
);
|
||||
|
||||
$powerWatts = max(0.0, (float) ($settings['power_watts'] ?? 0));
|
||||
$electricityPrice = max(0.0, (float) ($settings['electricity_price'] ?? 0));
|
||||
$poolFeePercent = max(0.0, min(100.0, (float) ($settings['pool_fee_percent'] ?? 0)));
|
||||
$coinPrice = max(0.0, (float) ($settings['coin_price'] ?? 0));
|
||||
$blockReward = max(0.0, (float) ($settings['block_reward'] ?? 0));
|
||||
$blockTimeSeconds = max(1.0, (float) ($settings['block_time_seconds'] ?? 60));
|
||||
$hardwareCost = max(0.0, (float) ($settings['hardware_cost'] ?? 0));
|
||||
|
||||
$share = $networkHashrate > 0 ? $hashrate / $networkHashrate : null;
|
||||
$blocksPerDay = 86400 / $blockTimeSeconds;
|
||||
$dailyCoinsGross = $share !== null ? $share * $blocksPerDay * $blockReward : null;
|
||||
$dailyCoinsNet = $dailyCoinsGross !== null ? $dailyCoinsGross * (1 - ($poolFeePercent / 100)) : null;
|
||||
$dailyRevenue = $dailyCoinsNet !== null ? $dailyCoinsNet * $coinPrice : null;
|
||||
$dailyPowerCost = ($powerWatts / 1000) * 24 * $electricityPrice;
|
||||
$dailyProfit = $dailyRevenue !== null ? $dailyRevenue - $dailyPowerCost : null;
|
||||
$monthlyProfit = $dailyProfit !== null ? $dailyProfit * 30 : null;
|
||||
$annualProfit = $dailyProfit !== null ? $dailyProfit * 365 : null;
|
||||
$breakEvenDays = ($dailyProfit !== null && $dailyProfit > 0.0 && $hardwareCost > 0.0)
|
||||
? $hardwareCost / $dailyProfit
|
||||
: null;
|
||||
$efficiency = $hashrate > 0 ? $powerWatts / ($hashrate / 1000000) : null;
|
||||
|
||||
return [
|
||||
'hashrate_hps' => $hashrate > 0 ? $hashrate : null,
|
||||
'network_hashrate_hps' => $networkHashrate > 0 ? $networkHashrate : null,
|
||||
'share_ratio' => $share,
|
||||
'blocks_per_day' => $blocksPerDay,
|
||||
'daily_coins_gross' => $dailyCoinsGross,
|
||||
'daily_coins_net' => $dailyCoinsNet,
|
||||
'daily_revenue' => $dailyRevenue,
|
||||
'daily_power_cost' => $dailyPowerCost,
|
||||
'daily_profit' => $dailyProfit,
|
||||
'monthly_profit' => $monthlyProfit,
|
||||
'annual_profit' => $annualProfit,
|
||||
'break_even_days' => $breakEvenDays,
|
||||
'efficiency_w_per_mh' => $efficiency,
|
||||
];
|
||||
}
|
||||
|
||||
private function toHashrateHps(float $value, string $unit): float
|
||||
{
|
||||
if ($value <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
$normalized = strtoupper(trim($unit));
|
||||
$multipliers = [
|
||||
'H/S' => 1,
|
||||
'KH/S' => 1000,
|
||||
'MH/S' => 1000000,
|
||||
'GH/S' => 1000000000,
|
||||
'TH/S' => 1000000000000,
|
||||
'PH/S' => 1000000000000000,
|
||||
'EH/S' => 1000000000000000000,
|
||||
];
|
||||
|
||||
$multiplier = $multipliers[$normalized] ?? 1000000;
|
||||
|
||||
return $value * $multiplier;
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
final class MiningCheckerStore
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $projectRoot,
|
||||
private readonly string $userScope,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function load(): array
|
||||
{
|
||||
$path = $this->path();
|
||||
|
||||
if (!is_file($path)) {
|
||||
return $this->defaultState();
|
||||
}
|
||||
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
return $this->defaultState();
|
||||
}
|
||||
|
||||
$data = json_decode($raw, true);
|
||||
|
||||
return is_array($data) ? array_replace_recursive($this->defaultState(), $data) : $this->defaultState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $state
|
||||
*/
|
||||
public function save(array $state): void
|
||||
{
|
||||
$directory = dirname($this->path());
|
||||
if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('Mining-Checker-Verzeichnis konnte nicht erstellt werden.');
|
||||
}
|
||||
|
||||
$json = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($json === false) {
|
||||
throw new RuntimeException('Mining-Checker-Daten konnten nicht serialisiert werden.');
|
||||
}
|
||||
|
||||
if (file_put_contents($this->path(), $json . PHP_EOL, LOCK_EX) === false) {
|
||||
throw new RuntimeException('Mining-Checker-Daten konnten nicht gespeichert werden.');
|
||||
}
|
||||
}
|
||||
|
||||
private function path(): string
|
||||
{
|
||||
$safeScope = preg_replace('/[^a-zA-Z0-9_-]+/', '-', $this->userScope) ?: 'guest';
|
||||
|
||||
return $this->projectRoot . '/data/mining-checker/' . $safeScope . '.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function defaultState(): array
|
||||
{
|
||||
return [
|
||||
'settings' => [
|
||||
'project_name' => 'DOGE Mining',
|
||||
'coin_symbol' => 'DOGE',
|
||||
'algorithm' => 'Scrypt',
|
||||
'currency' => 'EUR',
|
||||
'hashrate' => 950,
|
||||
'hashrate_unit' => 'MH/s',
|
||||
'network_hashrate' => 920,
|
||||
'network_hashrate_unit' => 'TH/s',
|
||||
'block_reward' => 10000,
|
||||
'block_time_seconds' => 60,
|
||||
'coin_price' => 0.14,
|
||||
'power_watts' => 3420,
|
||||
'electricity_price' => 0.32,
|
||||
'pool_fee_percent' => 1.5,
|
||||
'hardware_cost' => 8400,
|
||||
'notes' => '',
|
||||
],
|
||||
'history' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MiningChecker;
|
||||
|
||||
final class MiningCheckerUserScope
|
||||
{
|
||||
public static function current(): string
|
||||
{
|
||||
$auth = $_SESSION['desktop_auth'] ?? null;
|
||||
|
||||
if (!is_array($auth)) {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
$user = is_array($auth['user'] ?? null) ? $auth['user'] : [];
|
||||
|
||||
return (string) ($user['sub'] ?? $user['username'] ?? 'guest');
|
||||
}
|
||||
}
|
||||
71
src/ModulesCore/ModuleHttp.php
Normal file
71
src/ModulesCore/ModuleHttp.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ModulesCore;
|
||||
|
||||
use App\AccountGate;
|
||||
use App\ConfigLoader;
|
||||
use App\KeycloakAuth;
|
||||
|
||||
final class ModuleHttp
|
||||
{
|
||||
public static function requireDesktopAccess(string $projectRoot, bool $json = false): void
|
||||
{
|
||||
$auth = new KeycloakAuth(ConfigLoader::load($projectRoot, 'keycloak'));
|
||||
$accountGate = new AccountGate(ConfigLoader::load($projectRoot, 'registration'));
|
||||
|
||||
if ($auth->isAuthenticated()) {
|
||||
$currentUser = is_array($_SESSION['desktop_auth']['user'] ?? null) ? $_SESSION['desktop_auth']['user'] : [];
|
||||
$accountCheck = $accountGate->checkUsername((string) ($currentUser['username'] ?? ''));
|
||||
|
||||
if (!($accountCheck['allowed'] ?? false)) {
|
||||
$auth->logout();
|
||||
$message = (string) ($accountCheck['message'] ?? 'Dieses Konto ist noch nicht freigeschaltet.');
|
||||
|
||||
if ($json) {
|
||||
self::respondJson(['error' => $message], 403);
|
||||
}
|
||||
|
||||
$target = (string) ($accountCheck['state'] ?? '') === 'pending'
|
||||
? '/auth/pending/?username=' . urlencode((string) ($currentUser['username'] ?? ''))
|
||||
: '/auth/inactive/?message=' . urlencode($message);
|
||||
header('Location: ' . $target, true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$auth->shouldShowDesktop()) {
|
||||
if ($json) {
|
||||
self::respondJson(['error' => 'Nicht autorisiert.'], 401);
|
||||
}
|
||||
|
||||
header('Location: /auth/keycloak', true, 302);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
public static function currentUserScope(): string
|
||||
{
|
||||
$auth = $_SESSION['desktop_auth'] ?? null;
|
||||
|
||||
if (!is_array($auth)) {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
$user = is_array($auth['user'] ?? null) ? $auth['user'] : [];
|
||||
|
||||
return (string) ($user['sub'] ?? $user['username'] ?? 'guest');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private static function respondJson(array $payload, int $statusCode): never
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
105
src/ModulesCore/ModuleRegistry.php
Normal file
105
src/ModulesCore/ModuleRegistry.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ModulesCore;
|
||||
|
||||
final class ModuleRegistry
|
||||
{
|
||||
/** @var array<int, array<string, mixed>>|null */
|
||||
private ?array $desktopApps = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly string $modulesPath,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function desktopApps(): array
|
||||
{
|
||||
if ($this->desktopApps !== null) {
|
||||
return $this->desktopApps;
|
||||
}
|
||||
|
||||
$apps = [];
|
||||
|
||||
foreach ($this->moduleDirectories() as $moduleName => $modulePath) {
|
||||
$desktopConfigPath = $modulePath . '/desktop.php';
|
||||
if (!is_file($desktopConfigPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$desktopConfig = require $desktopConfigPath;
|
||||
if (!is_array($desktopConfig)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$manifest = $this->readJson($modulePath . '/module.json');
|
||||
$desktopConfig['module_id'] = $moduleName;
|
||||
$desktopConfig['title'] ??= (string) ($manifest['title'] ?? ucfirst($moduleName));
|
||||
$desktopConfig['summary'] ??= (string) ($manifest['description'] ?? '');
|
||||
$desktopConfig['required_groups'] ??= array_values(array_map(
|
||||
'strval',
|
||||
(array) ($manifest['auth']['groups'] ?? [])
|
||||
));
|
||||
$desktopConfig['enabled_by_default'] ??= (bool) ($manifest['enabled_by_default'] ?? false);
|
||||
$apps[] = $desktopConfig;
|
||||
}
|
||||
|
||||
return $this->desktopApps = $apps;
|
||||
}
|
||||
|
||||
public function modulePath(string $moduleName): ?string
|
||||
{
|
||||
$directories = $this->moduleDirectories();
|
||||
|
||||
return $directories[$moduleName] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function moduleDirectories(): array
|
||||
{
|
||||
$paths = glob(rtrim($this->modulesPath, '/') . '/*', GLOB_ONLYDIR);
|
||||
if (!is_array($paths)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$directories = [];
|
||||
|
||||
foreach ($paths as $path) {
|
||||
$moduleName = basename($path);
|
||||
if ($moduleName === '' || $moduleName[0] === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$directories[$moduleName] = $path;
|
||||
}
|
||||
|
||||
ksort($directories);
|
||||
|
||||
return $directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function readJson(string $path): array
|
||||
{
|
||||
if (!is_file($path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$raw = file_get_contents($path);
|
||||
if ($raw === false || trim($raw) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user