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 – Router, DI Container, Service Provider
|
||||||
milestone_2_core_infrastructure:
|
milestone_2_core_infrastructure:
|
||||||
status: TODO
|
status: COMPLETE
|
||||||
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.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,12 @@ $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);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -15,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;
|
||||||
|
|
@ -74,4 +74,4 @@ class Config
|
||||||
return $this->values;
|
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