Milestone 2
This commit is contained in:
parent
059d3d2f12
commit
350d7f6351
|
|
@ -60,7 +60,7 @@ milestones:
|
|||
|
||||
# Milestone 2: Core Infrastructure – Router, DI Container, Service Provider
|
||||
milestone_2_core_infrastructure:
|
||||
status: TODO
|
||||
status: COMPLETE
|
||||
description: >-
|
||||
Build the custom routing engine, lightweight dependency injection container,
|
||||
and service provider pattern that will be used throughout the application.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,12 @@ $mergedEnv = $_ENV + $_SERVER;
|
|||
|
||||
use League\Container\Container;
|
||||
use SiteWeaver\Core\Config;
|
||||
use SiteWeaver\Core\Router;
|
||||
$container = new Container();
|
||||
|
||||
// Register core services
|
||||
$router = new Router();
|
||||
$container->add('router', $router);
|
||||
$container->add('config', function () use ($mergedEnv) {
|
||||
return new Config($mergedEnv);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ class Config
|
|||
/** @var array<string, mixed> */
|
||||
private array $values = [];
|
||||
|
||||
public function __construct(array $env = null)
|
||||
public function __construct(?array $env = null)
|
||||
{
|
||||
// Use provided env array or fallback to $_ENV
|
||||
$this->values = $env ?? $_ENV;
|
||||
|
|
@ -74,4 +74,4 @@ class Config
|
|||
return $this->values;
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
||||
|
|
|
|||
103
src/Site/Core/Router.php
Normal file
103
src/Site/Core/Router.php
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
30
src/Site/Core/ServiceProvider.php
Normal file
30
src/Site/Core/ServiceProvider.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
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 {}
|
||||
}
|
||||
|
||||
33
tests/unit/Core/RouterTest.php
Normal file
33
tests/unit/Core/RouterTest.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?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);
|
||||
}
|
||||
}
|
||||
|
||||
26
tests/unit/Core/ServiceProviderTest.php
Normal file
26
tests/unit/Core/ServiceProviderTest.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?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'));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue