Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,4 @@ www/warnings.json
www/warnings_full.json
fppdLog.txt
_notes
.phpunit.result.cache
1 change: 1 addition & 0 deletions www/common.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<?php
require_once "config.php";
require_once __DIR__ . '/lib/bootstrap.php';

function check($var, $var_name = "", $function_name = "")
{
Expand Down
8 changes: 4 additions & 4 deletions www/cronjobs.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ static private function arrayToString($jobs = array())

static public function getJobs()
{
$output = shell_exec('crontab -l');
return self::stringToArray($output);
$result = fpp()->shell->run('crontab -l');
return self::stringToArray((string) $result);
}

static public function saveJobs($jobs = array())
{
$output = shell_exec('echo "' . self::arrayToString($jobs) . '" | crontab -');
return $output;
return (string) fpp()->shell
->run('echo "' . self::arrayToString($jobs) . '" | crontab -');
}

static public function doesJobExist($job = '')
Expand Down
170 changes: 170 additions & 0 deletions www/lib/Container/Container.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

namespace FalconChristmas\Fpp\Container;

use FalconChristmas\Fpp\Shell\ShellExecutor;
use FalconChristmas\Fpp\Container\Exception\ContainerException;
use FalconChristmas\Fpp\Container\Exception\NotFoundException;
use Throwable;

/**
* Minimal IoC container to centralize service access inside the web UI.
*
* @property ShellExecutor $shell
*/
class Container
{
/**
* @var array<string, mixed>
*/
private $bindings = [];

/**
* @var array<string, bool>
*/
private $singletons = [];

/**
* @var array<string, mixed>
*/
private $instances = [];

/**
* Register a service factory or instance.
*
* @param string $key
* @param mixed $resolver
*/
public function bind(string $key, mixed $resolver): void
{
$this->bindings[$key] = $resolver;
unset($this->singletons[$key], $this->instances[$key]);
}

/**
* Register a singleton factory or instance.
*
* @param string $key
* @param mixed $resolver
*/
public function singleton(string $key, mixed $resolver): void
{
$this->bindings[$key] = $resolver;
$this->singletons[$key] = true;
unset($this->instances[$key]);
}

/**
* Register an already instantiated singleton.
*
* @param string $key
* @param mixed $instance
*/
public function instance(string $key, mixed $instance): void
{
$this->instances[$key] = $instance;
$this->singletons[$key] = true;
unset($this->bindings[$key]);
}

/**
* Determine if a binding exists.
*
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return array_key_exists($key, $this->bindings) || array_key_exists($key, $this->instances);
}

/**
* Resolve a binding or singleton.
*
* @param string $key
* @return mixed
* @throws NotFoundException
* @throws ContainerException
*/
public function make(string $key): mixed
{
if (array_key_exists($key, $this->instances)) {
return $this->instances[$key];
}

if (!array_key_exists($key, $this->bindings)) {
throw new NotFoundException("Service '{$key}' not registered in the container.");
}

$resolver = $this->bindings[$key];
try {
$instance = $this->resolve($resolver);
} catch (Throwable $e) {
throw new ContainerException("Error resolving service '{$key}'.", 0, $e);
}

if (isset($this->singletons[$key])) {
$this->instances[$key] = $instance;
unset($this->bindings[$key]);
}

return $instance;
}

/**
* Remove a binding and its cached instance.
*
* @param string $key
*/
public function forget(string $key): void
{
unset($this->bindings[$key], $this->instances[$key], $this->singletons[$key]);
}

/**
* Remove all bindings, singletons, and cached instances.
*/
public function clear(): void
{
$this->bindings = [];
$this->singletons = [];
$this->instances = [];
}

/**
* Magic getter for syntactic sugar: $container->service
*
* @param string $key
* @return mixed
*/
public function __get(string $key): mixed
{
return $this->make($key);
}

/**
* PSR-11 compatibility.
*
* @param string $id
* @return mixed
*/
public function get(string $id): mixed
{
return $this->make($id);
}

/**
* Resolve the value for a binding.
*
* @param mixed $resolver
* @return mixed
*/
private function resolve(mixed $resolver): mixed
{
if (is_callable($resolver)) {
return $resolver($this);
}

return $resolver;
}
}
9 changes: 9 additions & 0 deletions www/lib/Container/Exception/ContainerException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace FalconChristmas\Fpp\Container\Exception;

use RuntimeException;

class ContainerException extends RuntimeException
{
}
9 changes: 9 additions & 0 deletions www/lib/Container/Exception/NotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace FalconChristmas\Fpp\Container\Exception;

use RuntimeException;

class NotFoundException extends RuntimeException
{
}
27 changes: 27 additions & 0 deletions www/lib/Shell/ShellExecutor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace FalconChristmas\Fpp\Shell;

/**
* Thin wrapper around exec() to keep shell interactions centralized/testable.
*/
class ShellExecutor
{
/**
* @param string $command
* @param bool $redirectStdErr
* @return ShellResult
*/
public function run(string $command, bool $redirectStdErr = true): ShellResult
{
if ($redirectStdErr) {
$command .= ' 2>&1';
}

$output = [];
$exitCode = 0;
exec($command, $output, $exitCode);

return new ShellResult($command, $output, $exitCode);
}
}
62 changes: 62 additions & 0 deletions www/lib/Shell/ShellResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace FalconChristmas\Fpp\Shell;

class ShellResult
{
private string $command;
private array $output;
private int $exitCode;

/**
* @param string $command
* @param array<int, string> $output
* @param int $exitCode
*/
public function __construct(string $command, array $output, int $exitCode)
{
$this->command = $command;
$this->output = $output;
$this->exitCode = $exitCode;
}

/**
* @return string
*/
public function getCommand(): string
{
return $this->command;
}

/**
* @return array<int, string>
*/
public function getOutput(): array
{
return $this->output;
}

/**
* @return int
*/
public function getExitCode(): int
{
return $this->exitCode;
}

/**
* @return bool
*/
public function wasSuccessful(): bool
{
return $this->exitCode === 0;
}

/**
* @return string
*/
public function __toString(): string
{
return implode("\n", $this->output);
}
}
43 changes: 43 additions & 0 deletions www/lib/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

use FalconChristmas\Fpp\Container\Container;
use FalconChristmas\Fpp\Shell\ShellExecutor;

spl_autoload_register(function ($class) {
$prefix = 'FalconChristmas\\Fpp\\';
$baseDir = __DIR__ . '/';

if (!startsWith($class, $prefix)) {
return;
}

$relativeClass = substr($class, strlen($prefix));
$file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

if (file_exists($file)) {
require $file;
}
});

if (!function_exists('fpp')) {
/**
* Global helper to access the shared service container.
*
* @return \FalconChristmas\Fpp\Container\Container
*/
function fpp()
{
static $container = null;

if ($container === null) {
$container = new Container();

// Default service bindings; tests can override them as needed.
$container->singleton('shell', function () {
return new ShellExecutor();
});
}

return $container;
}
}
13 changes: 13 additions & 0 deletions www/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit
bootstrap="tests/bootstrap.php"
colors="true"
beStrictAboutTestsThatDoNotTestAnything="true"
backupGlobals="false"
>
<testsuites>
<testsuite name="FPP Test Suite">
<directory>tests</directory>
</testsuite>
</testsuites>
</phpunit>
Loading