233 lines
8.2 KiB
PHP
233 lines
8.2 KiB
PHP
<?php
|
||
|
||
namespace App\Commands;
|
||
|
||
use App\Models\ConfigurationModel;
|
||
use CodeIgniter\CLI\BaseCommand;
|
||
use CodeIgniter\CLI\CLI;
|
||
use DateTime;
|
||
use DateTimeZone;
|
||
|
||
class ConfigUpdate extends BaseCommand
|
||
{
|
||
protected $group = 'Maintenance';
|
||
protected $name = 'config:update';
|
||
protected $description = 'Run a configuration update task (weekly cron).';
|
||
protected $arguments = [];
|
||
protected $usage = 'php spark config:update [task] [--task task|-t task] [--dry] [--force] [--tz=<timezone>]';
|
||
protected $options = [
|
||
'task' => 'Task name (or pass as first positional arg)',
|
||
't' => 'Short form of --task',
|
||
'dry' => 'Dry run (no DB writes)',
|
||
'force' => 'Ignore lock and run anyway',
|
||
'tz' => 'Timezone (default: configured school timezone)',
|
||
];
|
||
|
||
|
||
/** @var ConfigurationModel */
|
||
protected $configModel;
|
||
|
||
/** Map of available task names -> handler methods. */
|
||
protected array $tasks = [
|
||
'update_date_age_reference' => 'taskUpdateDateAgeReference',
|
||
'enable_attendance_on' => 'taskEnableAttendanceOn',
|
||
'enable_attendance_off' => 'taskEnableAttendanceOff',
|
||
'set_semester_spring' => 'taskSetSemesterSpring',
|
||
'set_semester_fall' => 'taskSetSemesterFall',
|
||
];
|
||
|
||
|
||
/**
|
||
* Set enable_attendance = 1
|
||
*/
|
||
protected function taskEnableAttendanceOn(bool $dry, DateTimeZone $tz): bool
|
||
{
|
||
return $this->setConfig('enable_attendance', '1', $dry);
|
||
}
|
||
|
||
/**
|
||
* Set enable_attendance = 0
|
||
*/
|
||
protected function taskEnableAttendanceOff(bool $dry, DateTimeZone $tz): bool
|
||
{
|
||
return $this->setConfig('enable_attendance', '0', $dry);
|
||
}
|
||
|
||
protected function taskUpdateDateAgeReference(bool $dry, DateTimeZone $tz): bool
|
||
{
|
||
$now = new DateTime('now', $tz);
|
||
$isJune1 = ($now->format('n') === '6' && $now->format('j') === '1');
|
||
$forced = (CLI::getOption('force') !== null);
|
||
|
||
if (!$isJune1 && !$forced) {
|
||
CLI::write(
|
||
"Today is {$now->format('Y-m-d')} (not June 1) — skipping. Use --force to override.",
|
||
'yellow'
|
||
);
|
||
return true; // no-op, not an error
|
||
}
|
||
|
||
$year = (int) $now->format('Y');
|
||
$value = sprintf('%04d-12-31', $year);
|
||
|
||
CLI::write("Set date_age_reference = {$value}" . ($dry ? ' [DRY]' : ''), 'light_gray');
|
||
if ($dry) return true;
|
||
|
||
// Use your model method
|
||
$ok = (bool) $this->configModel->setConfigValueByKey('date_age_reference', $value);
|
||
|
||
if ($ok) {
|
||
CLI::write("date_age_reference updated to {$value}", 'green');
|
||
} else {
|
||
CLI::error("Failed to update date_age_reference");
|
||
}
|
||
return $ok;
|
||
}
|
||
|
||
protected function taskSetSemesterSpring(bool $dry, DateTimeZone $tz): bool
|
||
{
|
||
$now = new DateTime('now', $tz);
|
||
$isFeb1 = ($now->format('n') === '2' && $now->format('j') === '1');
|
||
$forced = (CLI::getOption('force') !== null);
|
||
|
||
if (!$isFeb1 && !$forced) {
|
||
CLI::write(
|
||
"Today is {$now->format('Y-m-d')} (not Feb 1) — skipping. Use --force to override.",
|
||
'yellow'
|
||
);
|
||
return true; // no-op is success
|
||
}
|
||
|
||
CLI::write("Set semester = Spring" . ($dry ? ' [DRY]' : ''), 'light_gray');
|
||
if ($dry) return true;
|
||
|
||
return (bool) $this->configModel->setConfigValueByKey('semester', 'Spring');
|
||
}
|
||
|
||
protected function taskSetSemesterFall(bool $dry, DateTimeZone $tz): bool
|
||
{
|
||
$now = new DateTime('now', $tz);
|
||
$isJun1 = ($now->format('n') === '6' && $now->format('j') === '1');
|
||
$forced = (CLI::getOption('force') !== null);
|
||
|
||
if (!$isJun1 && !$forced) {
|
||
CLI::write(
|
||
"Today is {$now->format('Y-m-d')} (not Jun 1) — skipping. Use --force to override.",
|
||
'yellow'
|
||
);
|
||
return true;
|
||
}
|
||
|
||
CLI::write("Set semester = Fall" . ($dry ? ' [DRY]' : ''), 'light_gray');
|
||
if ($dry) return true;
|
||
|
||
return (bool) $this->configModel->setConfigValueByKey('semester', 'Fall');
|
||
}
|
||
|
||
public function run(array $params)
|
||
{
|
||
$this->configModel = model(ConfigurationModel::class);
|
||
|
||
$tz = $this->getOptionString('tz', $params) ?? ((string)(config('School')->attendance['timezone'] ?? 'UTC'));
|
||
$task = $this->getOptionString('task', $params, 't');
|
||
|
||
// Fallback: first positional arg (php spark config:update enable_attendance_on)
|
||
if (!$task && !empty($params) && strpos($params[0], '-') !== 0) {
|
||
$task = trim($params[0]);
|
||
}
|
||
|
||
$dry = $this->hasFlag('dry', $params);
|
||
$force = $this->hasFlag('force', $params);
|
||
$dtz = new DateTimeZone($tz);
|
||
|
||
if ($task === '' || !isset($this->tasks[$task])) {
|
||
CLI::error('Invalid or missing --task. Available: ' . implode(', ', array_keys($this->tasks)));
|
||
return;
|
||
}
|
||
|
||
// Per-task lock
|
||
$lockFile = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "ci4_config_update_{$task}.lock";
|
||
$fp = @fopen($lockFile, 'c+');
|
||
if (!$fp) {
|
||
CLI::error("Unable to open lock file: $lockFile");
|
||
return;
|
||
}
|
||
if (!$force && !flock($fp, LOCK_EX | LOCK_NB)) {
|
||
CLI::error("Task '$task' is already running (lock held). Use --force to override.");
|
||
fclose($fp);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
$method = $this->tasks[$task];
|
||
CLI::write("Running task: {$task}" . ($dry ? ' [DRY RUN]' : ''), 'yellow');
|
||
$ok = $this->{$method}($dry, $dtz);
|
||
if ($ok === true) {
|
||
CLI::write("Task '{$task}' finished successfully.", 'green');
|
||
} else {
|
||
CLI::error("Task '{$task}' finished with warnings or no changes.");
|
||
}
|
||
} catch (\Throwable $e) {
|
||
CLI::error("Task '{$task}' failed: " . $e->getMessage());
|
||
} finally {
|
||
try {
|
||
flock($fp, LOCK_UN);
|
||
fclose($fp);
|
||
@unlink($lockFile);
|
||
} catch (\Throwable $e) {
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Accepts --name=value, --name value, -s value, or scans $params. */
|
||
private function getOptionString(string $name, array $params, ?string $short = null): ?string
|
||
{
|
||
$v = CLI::getOption($name);
|
||
if (is_string($v) && $v !== '') return trim($v);
|
||
if ($short) {
|
||
$v = CLI::getOption($short);
|
||
if (is_string($v) && $v !== '') return trim($v);
|
||
}
|
||
foreach ($params as $i => $p) {
|
||
if (strpos($p, "--{$name}=") === 0) return trim(substr($p, strlen($name) + 3));
|
||
if ($short && strpos($p, "-{$short}=") === 0) return trim(substr($p, strlen($short) + 2));
|
||
if ($p === "--{$name}" || ($short && $p === "-{$short}")) {
|
||
return $params[$i + 1] ?? null;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/** True if flag present as --name or -s (no value needed). */
|
||
private function hasFlag(string $name, array $params, ?string $short = null): bool
|
||
{
|
||
if (CLI::getOption($name) !== null) return true;
|
||
if ($short && CLI::getOption($short) !== null) return true;
|
||
foreach ($params as $p) {
|
||
if ($p === "--{$name}" || ($short && $p === "-{$short}")) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
|
||
/* ------------------------- Helpers ------------------------- */
|
||
protected function setConfig(string $key, string $value, bool $dry): bool
|
||
{
|
||
// show current value
|
||
$current = $this->configModel->where('config_key', $key)->first()['config_value'] ?? '<NULL>';
|
||
CLI::write("{$key}: current={$current}", 'light_gray');
|
||
|
||
CLI::write("Set {$key} = {$value}" . ($dry ? ' [DRY]' : ''), 'light_gray');
|
||
if ($dry) return true;
|
||
|
||
// ✅ use your model function
|
||
$ok = (bool) $this->configModel->setConfigValueByKey($key, $value);
|
||
|
||
// read-back to verify what’s persisted
|
||
$after = $this->configModel->where('config_key', $key)->first()['config_value'] ?? '<NULL>';
|
||
CLI::write("{$key}: after={$after}", $ok ? 'green' : 'red');
|
||
|
||
return $ok && ($after === $value);
|
||
}
|
||
}
|