Skip to content

Commit

Permalink
Merge pull request #1 from sanderdlm/psr7
Browse files Browse the repository at this point in the history
Switch to a PSR-7 implementation
  • Loading branch information
sanderdlm authored Nov 26, 2023
2 parents e1d45bb + b738050 commit 5f22453
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 37 deletions.
32 changes: 15 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,56 +2,54 @@

Mono is a tiny, single-class PHP framework for writing single-page PHP apps.
It shines when quickly developing small tools with limited scope.
In +- 70 LOC, you get basic routing (using FastRoute), DI (using PHP-DI),
and Twig templating. Anything else you need, you have to bring yourself.
In +- 100 LOC, you get basic routing (using FastRoute), DI (using PHP-DI),
a PSR-7 implementation and Twig templating.

## Routing
Mono's routing implementation shares 90% of its code with the ['basic usage example'](https://github.com/nikic/FastRoute#usage) from the FastRoute documentation.

You use `addRoute()` to add all your routes. Same method signature as the FastRoute method. Route handlers are closures by default, since this is a single-page framework.
You use `$mono->addRoute()` to add all your routes. Same method signature as the FastRoute method. Route handlers are closures by default, since this is a single-page framework.

Read about the route pattern in the [FastRoute documentation](https://github.com/nikic/FastRoute#defining-routes). The entered path is passed directly to FastRoute.

When `$mono->run()` is called, the current request is matched against the routes you added, the closure is invoked and the string result is echo'd.
The first argument to the closure is the always current request, which is a [PSR-7 ServerRequestInterface](https://github.com/php-fig/http-message/blob/master/src/ServerRequestInterface.php) object. After that, the next arguments are the route parameters.

When `$mono->run()` is called, the current request is matched against the routes you added, the closure is invoked and the response is emitted.

```php
use App\Mono;$mono = new Mono();
$mono = new Mono();

$mono->addRoute('GET', '/', function() use ($mono) {
return 'Hello world!';
$mono->addRoute('GET', '/books/{book}', function(RequestInterface $request, string $book) use ($mono) {
return $mono->createResponse('Book: ' . $book);
});

echo $mono->run();
$mono->run();
```

Mono does not implement PSR-7 because it would be overkill for most tools you'd build in a single page .

If you do need status codes and headers, you can use `http_response_code()` and `header()` to do so.

## DI

When a Mono object is created, it constructs a basic PHP-DI container with default configuration. This means dependencies from your vendor folder are autowired.

You can fetch instances from the container with the `get()` method on your Mono object.

```php
use App\Mono;$mono = new Mono();
$mono = new Mono();

$mono->addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();

return json_encode($result);
return $mono->createResponse(json_encode($result));
});

echo $mono->run();
$mono->run();
```

## Twig

Mono comes with Twig out-of-the-box. You can use the `render()` method on your Mono object to render a Twig template.

```php
use App\Mono;$mono = new Mono();
$mono = new Mono();

$mono->addRoute('GET', '/example', function() use ($mono) {
$result = $mono->get(SomeDependency::class)->doSomething();
Expand All @@ -61,7 +59,7 @@ $mono->addRoute('GET', '/example', function() use ($mono) {
]);
});

echo $mono->run();
$mono->run();
````

Templates go in the `/templates` folder in the project root.
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@
"php": "^8.2 || ^8.3",
"nikic/fast-route": "^1.3",
"twig/twig": "^3.7",
"php-di/php-di": "^7.0"
"php-di/php-di": "^7.0",
"nyholm/psr7-server": "^1.1",
"nyholm/psr7": "^1.8",
"laminas/laminas-httphandlerrunner": "^2.9"
},
"require-dev": {
"symfony/var-dumper": "^6.3",
Expand Down
49 changes: 34 additions & 15 deletions src/Mono.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

use DI\Container;
use FastRoute;
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7\Response;
use Nyholm\Psr7Server\ServerRequestCreator;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Twig\Environment;
use Twig\Extension\DebugExtension;
use Twig\Loader\FilesystemLoader;
Expand Down Expand Up @@ -51,41 +56,55 @@ public function addRoute(string $method, string $path, callable $handler): void
/**
* @param array<string, mixed> $data
*/
public function render(string $template, array $data = []): string
public function render(string $template, array $data = []): ResponseInterface
{
$template = $this->twig->load($template);

return $template->render($data);
$output = $template->render($data);

return $this->createResponse($output);
}

public function get(string $className): mixed
{
return $this->container->get($className);
}

public function run(): mixed
public function createResponse(string $body, int $status = 200): Response
{
$psr17Factory = new Psr17Factory();

return (new Response($status))->withBody($psr17Factory->createStream($body));
}

public function run(): void
{
$dispatcher = FastRoute\simpleDispatcher(function (FastRoute\RouteCollector $r) {
foreach ($this->routes as $route) {
$r->addRoute($route['method'], $route['path'], $route['handler']);
}
});

$httpMethod = $_SERVER['REQUEST_METHOD'];
$uri = $_SERVER['REQUEST_URI'];
$psr17Factory = new Psr17Factory();

if (false !== $pos = strpos($uri, '?')) {
$uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);
$creator = new ServerRequestCreator($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$request = $creator->fromGlobals();

$routeInfo = $dispatcher->dispatch($httpMethod, $uri);
$route = $dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());

return match ($routeInfo[0]) {
FastRoute\Dispatcher::NOT_FOUND => '404 Not Found',
FastRoute\Dispatcher::METHOD_NOT_ALLOWED => '405 Method Not Allowed',
FastRoute\Dispatcher::FOUND => call_user_func_array($routeInfo[1], $routeInfo[2]),
default => throw new \RuntimeException('Something went wrong'),
/** @var ?ResponseInterface $response */
$response = match ($route[0]) {
FastRoute\Dispatcher::NOT_FOUND => $this->createResponse('404 Not Found', 404),
FastRoute\Dispatcher::METHOD_NOT_ALLOWED => $this->createResponse('405 Method Not Allowed', 405),
FastRoute\Dispatcher::FOUND => call_user_func_array($route[1], [$request, ...$route[2]]),
default => $this->createResponse('500 Internal Server Error', 500)
};

if (!$response instanceof ResponseInterface) {
throw new \RuntimeException('Invalid response received from route ' . $request->getUri()->getPath() .
'. Please return a valid PSR-7 response from your handler.');
}

(new SapiEmitter())->emit($response);
}
}
56 changes: 52 additions & 4 deletions tests/MonoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,55 @@
use Mono\Mono;
use PhpParser\NodeTraverser;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\RequestInterface;

class MonoTest extends TestCase
{
private function catchOutput(callable $run): string
{
ob_start();

$run();

$output = ob_get_contents();

ob_end_clean();

return !$output ? '' : $output;
}

public function testRouting(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';

$mono = new Mono(__DIR__);

$mono->addRoute('GET', '/', function () {
return 'Hello, world!';
$mono->addRoute('GET', '/', function (RequestInterface $request) use ($mono) {
return $mono->createResponse('Hello, world!');
});

$output = $mono->run();
$output = $this->catchOutput(fn() => $mono->run());

$this->assertEquals('Hello, world!', $output);
}

public function testRoutingWithParameters(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/books/123';

$mono = new Mono(__DIR__);

$mono->addRoute('GET', '/books/{book}', function (RequestInterface $request, string $book) use ($mono) {
return $mono->createResponse('Book: ' . $book);
});

$output = $this->catchOutput(fn() => $mono->run());

$this->assertEquals('Book: 123', $output);
}

public function testTwigRendering(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
Expand All @@ -37,7 +67,7 @@ public function testTwigRendering(): void
]);
});

$output = $mono->run();
$output = $this->catchOutput(fn() => $mono->run());

$this->assertEquals('Hello, world!', $output);
}
Expand All @@ -53,8 +83,26 @@ public function testDependencyInjection(): void
$demoClass = $mono->get(NodeTraverser::class);

$this->assertInstanceOf(NodeTraverser::class, $demoClass);

return $mono->createResponse('OK');
});

$mono->run();
}

public function testInvalidResponseThrowError(): void
{
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/';

$mono = new Mono(__DIR__);

$mono->addRoute('GET', '/', function () {
return 'OK';
});

$this->expectException(\RuntimeException::class);

$mono->run();
}
}

0 comments on commit 5f22453

Please sign in to comment.