Compare commits
No commits in common. "42df7cac0ff952771ed93ac4c36c03eee7cccceb" and "059d3d2f122526902be1c44c3cae9ff720afd002" have entirely different histories.
42df7cac0f
...
059d3d2f12
|
|
@ -60,7 +60,7 @@ milestones:
|
||||||
|
|
||||||
# Milestone 2: Core Infrastructure – Router, DI Container, Service Provider
|
# Milestone 2: Core Infrastructure – Router, DI Container, Service Provider
|
||||||
milestone_2_core_infrastructure:
|
milestone_2_core_infrastructure:
|
||||||
status: COMPLETE
|
status: TODO
|
||||||
description: >-
|
description: >-
|
||||||
Build the custom routing engine, lightweight dependency injection container,
|
Build the custom routing engine, lightweight dependency injection container,
|
||||||
and service provider pattern that will be used throughout the application.
|
and service provider pattern that will be used throughout the application.
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,2 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: _bootstrap.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Bootstrap file for the application.
|
|
||||||
*/
|
|
||||||
require __DIR__ . '/vendor/autoload.php';
|
require __DIR__ . '/vendor/autoload.php';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: config/bootstrap.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Bootstrap configuration and DI container.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
@ -19,12 +10,7 @@ $mergedEnv = $_ENV + $_SERVER;
|
||||||
|
|
||||||
use League\Container\Container;
|
use League\Container\Container;
|
||||||
use SiteWeaver\Core\Config;
|
use SiteWeaver\Core\Config;
|
||||||
use SiteWeaver\Core\Router;
|
|
||||||
$container = new Container();
|
$container = new Container();
|
||||||
|
|
||||||
// Register core services
|
|
||||||
$router = new Router();
|
|
||||||
$container->add('router', $router);
|
|
||||||
$container->add('config', function () use ($mergedEnv) {
|
$container->add('config', function () use ($mergedEnv) {
|
||||||
return new Config($mergedEnv);
|
return new Config($mergedEnv);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: public/index.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Front controller entry point that outputs a simple message.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
$container = require __DIR__ . '/../config/bootstrap.php';
|
$container = require __DIR__ . '/../config/bootstrap.php';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: src/Site/Core/Config.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Configuration service that loads environment variables and optionally fetches secrets from Infisical.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace SiteWeaver\Core;
|
namespace SiteWeaver\Core;
|
||||||
|
|
@ -24,7 +15,7 @@ class Config
|
||||||
/** @var array<string, mixed> */
|
/** @var array<string, mixed> */
|
||||||
private array $values = [];
|
private array $values = [];
|
||||||
|
|
||||||
public function __construct(?array $env = null)
|
public function __construct(array $env = null)
|
||||||
{
|
{
|
||||||
// Use provided env array or fallback to $_ENV
|
// Use provided env array or fallback to $_ENV
|
||||||
$this->values = $env ?? $_ENV;
|
$this->values = $env ?? $_ENV;
|
||||||
|
|
@ -83,4 +74,4 @@ class Config
|
||||||
return $this->values;
|
return $this->values;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
?>
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* File: src/Site/Core/Router.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Very small custom router.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace SiteWeaver\Core;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Very small custom router.
|
|
||||||
*/
|
|
||||||
class Router
|
|
||||||
{
|
|
||||||
/** @var array<int, array{method:string,path:string,handler:callable,middleware?:array<callable>}> */
|
|
||||||
private array $routes = [];
|
|
||||||
|
|
||||||
/** @var array<string,string> mapping of route name to path pattern */
|
|
||||||
private array $namedRoutes = [];
|
|
||||||
|
|
||||||
public function get(string $path, callable $handler, array $middleware = [], ?string $name = null): void
|
|
||||||
{
|
|
||||||
$this->addRoute('GET', $path, $handler, $middleware, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function post(string $path, callable $handler, array $middleware = [], ?string $name = null): void
|
|
||||||
{
|
|
||||||
$this->addRoute('POST', $path, $handler, $middleware, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function put(string $path, callable $handler, array $middleware = [], ?string $name = null): void
|
|
||||||
{
|
|
||||||
$this->addRoute('PUT', $path, $handler, $middleware, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(string $path, callable $handler, array $middleware = [], ?string $name = null): void
|
|
||||||
{
|
|
||||||
$this->addRoute('DELETE', $path, $handler, $middleware, $name);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function addRoute(
|
|
||||||
string $method,
|
|
||||||
string $path,
|
|
||||||
callable $handler,
|
|
||||||
array $middleware,
|
|
||||||
?string $name = null
|
|
||||||
): void {
|
|
||||||
$this->routes[] = [
|
|
||||||
'method' => strtoupper($method),
|
|
||||||
'path' => $path,
|
|
||||||
'handler' => $handler,
|
|
||||||
'middleware' => $middleware,
|
|
||||||
];
|
|
||||||
if ($name !== null) {
|
|
||||||
$this->namedRoutes[$name] = $path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dispatch a request.
|
|
||||||
*
|
|
||||||
* @return array{handler:callable,middleware:array<callable>,params:array<string,string>}|null
|
|
||||||
*/
|
|
||||||
public function dispatch(string $method, string $uri): ?array
|
|
||||||
{
|
|
||||||
foreach ($this->routes as $route) {
|
|
||||||
if (strtoupper($method) !== strtoupper($route['method'])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Build regex for path pattern
|
|
||||||
$pattern = preg_replace('#\{([^}]+)\}#', '(?P<$1>[^/]+)', $route['path']);
|
|
||||||
$pattern = '#^' . $pattern . '$#';
|
|
||||||
if (preg_match($pattern, $uri, $matches)) {
|
|
||||||
// filter named groups
|
|
||||||
$params = array_filter(
|
|
||||||
$matches,
|
|
||||||
fn ($k) => is_string($k),
|
|
||||||
ARRAY_FILTER_USE_KEY
|
|
||||||
);
|
|
||||||
return [
|
|
||||||
'handler' => $route['handler'],
|
|
||||||
'middleware' => $route['middleware'] ?? [],
|
|
||||||
'params' => $params,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a URL for a named route.
|
|
||||||
*/
|
|
||||||
public function url(string $name, array $params = []): string
|
|
||||||
{
|
|
||||||
if (!isset($this->namedRoutes[$name])) {
|
|
||||||
throw new \InvalidArgumentException("Route name {$name} not defined");
|
|
||||||
}
|
|
||||||
$path = $this->namedRoutes[$name];
|
|
||||||
foreach ($params as $key => $value) {
|
|
||||||
// replace both required and optional placeholders
|
|
||||||
$path = str_replace('{' . $key . '}', (string)$value, $path);
|
|
||||||
$path = str_replace('{?' . $key . '}', (string)$value, $path);
|
|
||||||
}
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* File: src/Site/Core/ServiceProvider.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Base class for all service providers.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace SiteWeaver\Core;
|
|
||||||
|
|
||||||
use League\Container\Container;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for all service providers.
|
|
||||||
*/
|
|
||||||
abstract class ServiceProvider
|
|
||||||
{
|
|
||||||
protected Container $container;
|
|
||||||
|
|
||||||
public function __construct(Container $container)
|
|
||||||
{
|
|
||||||
$this->container = $container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register bindings in the container.
|
|
||||||
*/
|
|
||||||
abstract public function register(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional boot method called after all providers are registered.
|
|
||||||
*/
|
|
||||||
public function boot(): void {}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,11 +1,2 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: tests/_bootstrap.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Bootstrap file for Codeception tests.
|
|
||||||
*/
|
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: tests/_support/UnitTester.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Unit tester support class for Codeception.
|
|
||||||
*/
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\unit\Core;
|
|
||||||
|
|
||||||
use SiteWeaver\Core\Router;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
final class RouterTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testRouteRegistrationAndDispatch(): void
|
|
||||||
{
|
|
||||||
$router = new Router();
|
|
||||||
$router->get('/users/{id}', fn($params) => 'handler', [], 'user_show');
|
|
||||||
|
|
||||||
$result = $router->dispatch('GET', '/users/42');
|
|
||||||
$this->assertNotNull($result);
|
|
||||||
$handler = $result['handler'];
|
|
||||||
$this->assertIsCallable($handler);
|
|
||||||
$this->assertSame('handler', ($handler)(['id' => '42']));
|
|
||||||
$this->assertEquals(['id' => '42'], $result['params']);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testUrlGeneration(): void
|
|
||||||
{
|
|
||||||
$router = new Router();
|
|
||||||
$router->get('/posts/{slug}', fn() => null, [], 'post_show');
|
|
||||||
|
|
||||||
$url = $router->url('post_show', ['slug' => 'hello-world']);
|
|
||||||
$this->assertSame('/posts/hello-world', $url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\unit\Core;
|
|
||||||
|
|
||||||
use SiteWeaver\Core\ServiceProvider;
|
|
||||||
use League\Container\Container;
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
|
|
||||||
final class DummyProvider extends ServiceProvider {
|
|
||||||
public function register(): void {
|
|
||||||
$this->container->add('dummy', fn() => 'value');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ServiceProviderTest extends TestCase
|
|
||||||
{
|
|
||||||
public function testRegisterAddsService(): void
|
|
||||||
{
|
|
||||||
$container = new Container();
|
|
||||||
$provider = new DummyProvider($container);
|
|
||||||
$provider->register();
|
|
||||||
|
|
||||||
$this->assertSame('value', $container->get('dummy'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* File: tests/unit/UnitTester.php
|
|
||||||
*
|
|
||||||
* Author: Funky Waddle
|
|
||||||
* Date: 2026-09-10
|
|
||||||
*
|
|
||||||
* Purpose:
|
|
||||||
* Test actor used by Codeception unit suite.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Tests\unit;
|
namespace Tests\unit;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue