TODO: Replace stubs

This commit is contained in:
2025-04-25 19:42:16 -04:00
commit 9bdecb1455
16 changed files with 2996 additions and 0 deletions

2
.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
.idea/
vendor/

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.idea/
vendor/

23
.rr.yaml Normal file
View File

@@ -0,0 +1,23 @@
version: "3"
server:
command: "php server.php"
rpc:
listen: tcp://127.0.0.1:6001
http:
pool:
allocate_timeout: 5s
reset_timeout: 5s
destroy_timeout: 5s
stream_timeout: 5s
num_workers: ${WORKERS:-4}
address: 0.0.0.0:${HTTP_PORT:-9501}
access_logs: ${ACCESS_LOGS:-true}
logs:
encoding: json
level: ${LOG_LEVEL:-info}
mode: production

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
# Use the RoadRunner image as a base for the first stage
FROM ghcr.io/roadrunner-server/roadrunner:2024.3.5 AS roadrunner
# Use the official Composer image as the base for the library stage
FROM siteworxpro/composer AS library
# Add Composer configuration files to the working directory
ADD composer.json composer.lock ./
# Install PHP dependencies, ignoring platform requirements and excluding development dependencies
RUN composer install --optimize-autoloader --ignore-platform-reqs --no-dev
# Use the official PHP CLI image with Alpine Linux for the second stage
FROM php:8.4.6-alpine AS php
# Move the production PHP configuration file to the default location
RUN mv /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
&& apk add libpq-dev linux-headers --no-cache \
&& docker-php-ext-install pdo_pgsql sockets \
&& rm -rf /var/cache/apk/*
# Set the working directory to /app
WORKDIR /app
# Copy the RoadRunner binary from the first stage to the second stage
COPY --from=roadrunner /usr/bin/rr /usr/local/bin/rr
# Copy the installed PHP dependencies from the library stage
COPY --from=library /app/vendor /app/vendor
# Copy the RoadRunner configuration file and source
ADD src src/
ADD server.php .
ADD .rr.yaml .
EXPOSE 9501
# Entrypoint command to run the RoadRunner server with the specified configuration
ENTRYPOINT ["rr", "serve", "-c", ".rr.yaml", "-s"]

1
README.md Normal file
View File

@@ -0,0 +1 @@
# Template

18
composer.json Normal file
View File

@@ -0,0 +1,18 @@
{
"name": "siteworxpro/app",
"type": "project",
"autoload": {
"psr-4": {
"Siteworxpro\\App\\": "src/"
}
},
"require": {
"php": "^8.4",
"league/route": "^6.2",
"illuminate/database": "^12.10",
"spiral/roadrunner-http": "^3.5",
"nyholm/psr7": "^1.8",
"illuminate/support": "^v12.10.2",
"roadrunner-php/app-logger": "^1.2"
}
}

2466
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
server.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
use Siteworxpro\App\Server;
require __DIR__ . '/vendor/autoload.php';
// Instantiate the ExternalServer class
$server = new Server();
// Start the server
try {
$server->startServer();
} catch (JsonException $e) {
echo $e->getMessage();
exit(1);
}

44
src/Facades/Config.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Facades;
use Illuminate\Support\Facades\Facade;
/**
* Class Config
*
* This class serves as a facade for the configuration settings of the application.
* It extends the Facade class from the Illuminate\Support\Facades namespace.
*
* @method static string|int|bool|float get(string $key, string $castTo = 'string') Retrieve the configuration value for the given key.
*
* @package Siteworx\App\Facades
*/
class Config extends Facade
{
public const string HTTP_PORT = 'HTTP_PORT';
public const string LOG_LEVEL = 'LOG_LEVEL';
public const string DB_DRIVER = 'DB_DRIVER';
public const string DB_HOST = 'DB_HOST';
public const string DB_DATABASE = 'DB_DATABASE';
public const string DB_USER = 'DB_USER';
public const string DB_PASSWORD = 'DB_PASSWORD';
public const string DEV_MODE = 'DEV_MODE';
public static function getFacadeRoot(): \Siteworxpro\App\Helpers\Config
{
return new \Siteworxpro\App\Helpers\Config();
}
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return \Siteworxpro\App\Helpers\Config::class;
}
}

42
src/Facades/Logger.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Facades;
use Illuminate\Support\Facades\Facade;
use RoadRunner\Logger\Logger as RRLogger;
use Spiral\Goridge\RPC\RPC;
/**
* Class Logger
*
* This class serves as a facade for the Monolog logger.
* It extends the Facade class from the Illuminate\Support\Facades namespace.
*
* @method static info(string $message, array $context = []) Log an informational message.
* @method static error(string $message, array $context = []) Log an error message.
* @method static warning(string $message, array $context = []) Log a warning message.
*
* @package Siteworxpro\App\Facades
*/
class Logger extends Facade
{
public static function getFacadeRoot(): RRLogger
{
$rpc = RPC::create('tcp://127.0.0.1:6001');
return new RRLogger($rpc);
}
/**
* Get the registered name of the component.
*
* @return string The name of the component.
*/
protected static function getFacadeAccessor(): string
{
return RRLogger::class;
}
}

32
src/Helpers/Config.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
use Psr\Log\LogLevel;
use Siteworxpro\App\Facades\Config as FacadeConfig;
class Config
{
private const array DEFAULTS = [
FacadeConfig::DB_DRIVER => 'pgsql',
FacadeConfig::DB_HOST => 'localhost',
FacadeConfig::DB_DATABASE => 'siteworx',
FacadeConfig::DB_USER => 'siteworx',
FacadeConfig::DB_PASSWORD => 'password',
FacadeConfig::LOG_LEVEL => LogLevel::DEBUG,
FacadeConfig::HTTP_PORT => '9501',
FacadeConfig::DEV_MODE => true,
];
/**
* @param string $key
* @param string $castTo
* @return string|int|bool|float
*/
public function get(string $key, string $castTo = 'string'): string|int|bool|float
{
return Env::get($key, self::DEFAULTS[$key] ?? null, $castTo);
}
}

26
src/Helpers/Env.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Helpers;
abstract class Env
{
/**
* @param string $key
* @param null $default
* @param string $castTo
* @return float|bool|int|string
*/
public static function get(string $key, $default = null, string $castTo = 'string'): float | bool | int | string
{
$env = getenv($key) !== false ? getenv($key) : $default;
return match ($castTo) {
'bool', 'boolean' => (bool) $env,
'int', 'integer' => (int) $env,
'float' => (float) $env,
default => (string) $env,
};
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http;
use Nyholm\Psr7\Response;
/**
* Class JsonResponseFactory
*
* A factory class for creating JSON responses.
*/
class JsonResponseFactory
{
/**
* Create a JSON response with the given data and status code.
*
* @param mixed $data The data to include in the response.
* @param int $statusCode The HTTP status code for the response.
* @return Response The JSON response.
* @throws \JsonException
*/
public static function createJsonResponse(array $data, int $statusCode = 200): Response
{
return new Response(
status: $statusCode,
headers: [
'Content-Type' => 'application/json',
],
body: json_encode($data, JSON_THROW_ON_ERROR)
);
}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Http\Middleware;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Siteworxpro\App\Facades\Config;
/**
* Class CorsMiddleware
*
* Middleware to handle CORS (Cross-Origin Resource Sharing) requests.
* It checks the origin of the request and sets appropriate CORS headers
* in the response.
*/
class CorsMiddleware implements MiddlewareInterface
{
/**
* Process the incoming request and add CORS headers to the response.
*
* @param ServerRequestInterface $request The incoming request.
* @param RequestHandlerInterface $handler The request handler.
* @return ResponseInterface The response with CORS headers.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$origin = $request->getHeaderLine('Origin');
$allowedOrigins = array_map(
'trim', explode(
',',
Config::get('CORS_ALLOWED_ORIGINS', 'https://example.com,https://another.com')
));
$allowOrigin = in_array($origin, $allowedOrigins, true)
? $origin
: 'null';
if ($request->getMethod() === 'OPTIONS') {
$response = new Response(204);
} else {
$response = $handler->handle($request);
}
$response = $response
->withHeader('Access-Control-Allow-Origin', $allowOrigin)
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->withHeader(
'Access-Control-Allow-Headers',
$request->getHeaderLine('Access-Control-Request-Headers')
?: 'Content-Type, Authorization'
);
if (Config::get('CORS_ALLOW_CREDENTIALS', 'bool')) {
$response = $response->withHeader('Access-Control-Allow-Credentials', 'true');
}
$maxAge = Config::get('CORS_MAX_AGE') ?: '86400';
return $response->withHeader('Access-Control-Max-Age', $maxAge);
}
}

12
src/Models/Model.php Normal file
View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App\Models;
use Illuminate\Database\Eloquent\Model as ORM;
abstract class Model extends ORM
{
}

170
src/Server.php Normal file
View File

@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
namespace Siteworxpro\App;
use League\Route\Http\Exception\MethodNotAllowedException;
use League\Route\Http\Exception\NotFoundException;
use League\Route\Router;
use Nyholm\Psr7\Factory\Psr17Factory;
use Siteworxpro\App\Facades\Config;
use Siteworxpro\App\Facades\Logger;
use Siteworxpro\App\Http\JsonResponseFactory;
use Siteworxpro\App\Http\Middleware\CorsMiddleware;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\Worker;
/**
* Abstract class Server
*
* This abstract class serves as a base for creating server instances.
* It initializes the PSR-7 worker and router, and provides an abstract method
* for registering routes. It also includes a method to start the server and handle
* incoming requests.
*/
class Server
{
/**
* @var Router The router instance for handling routes.
*/
protected Router $router;
/**
* @var PSR7Worker The PSR-7 worker instance for handling HTTP requests.
*/
protected PSR7Worker $worker;
/**
* Server constructor.
*
* Initializes the server by booting the PSR-7 worker and router.
*/
public function __construct()
{
$this->boot();
}
/**
* Bootstraps the server by initializing the PSR-7 worker and router.
*
* This method sets up the PSR-7 worker and router instances, and registers
* the routes for the server. It should be called in the constructor of
* subclasses to ensure proper initialization.
*
* @return void
*/
private function boot(): void
{
$this->worker = new PSR7Worker(
Worker::create(),
new Psr17Factory(),
new Psr17Factory(),
new Psr17Factory()
);
$this->router = new Router();
$this->registerRoutes();
$this->bootModelCapsule();
}
/**
* Bootstraps the model capsule for database connections.
*
* This method sets up the database connection using the Eloquent ORM.
* It retrieves the database configuration from the Config facade and
* initializes the Eloquent capsule manager.
*
* @return void
*/
public function bootModelCapsule(): void
{
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => Config::get(Config::DB_DRIVER),
'host' => Config::get(Config::DB_HOST),
'database' => Config::get(Config::DB_DATABASE),
'username' => Config::get(Config::DB_USER),
'password' => Config::get(Config::DB_PASSWORD),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
}
/**
* Registers the routes for the server.
*
* This method is responsible for defining the routes that the server will handle.
* It should be implemented in subclasses to provide specific route definitions.
*
* @return void
*/
protected function registerRoutes(): void
{
$this->router->get('/', function () {
return JsonResponseFactory::createJsonResponse(['status_code' => 200, 'message' => 'Server is running']);
});
$this->router->middleware(new CorsMiddleware());
}
/**
* Starts the server and handles incoming requests.
*
* This method enters an infinite loop to continuously handle incoming HTTP requests.
* It decodes the request body, routes the request, and sends the response. It also handles
* exceptions and ensures proper cleanup after each request.
*
* @throws \JsonException If there is an error decoding the JSON request body.
*/
public function startServer(): void
{
Logger::info(sprintf('Server started: %s', microtime(true)));
Logger::info(sprintf('Server PID: %s', getmypid()));
Logger::info(sprintf('Server Listening on: %s:%s', Config::get('HTTP_HOST'), Config::get('HTTP_PORT')));
while (true) {
try {
$request = $this->worker->waitRequest();
if ($request === null) {
break;
}
$request = $request->withParsedBody(json_decode($request->getBody()->getContents(), true));
$response = $this->router->handle($request);
$this->worker->respond($response);
} catch (MethodNotAllowedException|NotFoundException) {
$this->worker->respond(
JsonResponseFactory::createJsonResponse(
['status_code' => 404, 'reason_phrase' => 'Not Found'],
404
)
);
} catch (\Throwable $e) {
Logger::error($e->getMessage());
Logger::error($e->getTraceAsString());
$json = ['status_code' => 500, 'reason_phrase' => 'Server Error'];
if (Config::get("DEV_MODE", 'bool')) {
$json = [
'status_code' => 500,
'reason_phrase' => 'Server Error',
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
];
}
$this->worker->respond(JsonResponseFactory::createJsonResponse($json, 500));
}
}
}
}