Skip to content

Commit

Permalink
add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Baptouuuu committed Dec 30, 2022
1 parent c67fb0e commit c323caf
Show file tree
Hide file tree
Showing 8 changed files with 970 additions and 1 deletion.
121 changes: 120 additions & 1 deletion README.md
Expand Up @@ -4,6 +4,12 @@
[![codecov](https://codecov.io/gh/Innmind/Framework/branch/develop/graph/badge.svg)](https://codecov.io/gh/Innmind/Framework)
[![Type Coverage](https://shepherd.dev/github/Innmind/Framework/coverage.svg)](https://shepherd.dev/github/Innmind/Framework)

Minimalist HTTP/CLI framework that accomodate to simple applications to complex ones via middlewares.

The framework configuration is immutable and use a declarative approach.

**Important**: to correctly use this library you must validate your code with [`vimeo/psalm`](https://packagist.org/packages/vimeo/psalm)

## Installation

```sh
Expand All @@ -12,4 +18,117 @@ composer require innmind/framework

## Usage

TODO
Take a look at the [documentation](docs/) for a more in-depth understanding of the framework.

### Http

The first step is to create the index file that will be exposed via a webserver (for example `public/index.php`). Then you need to specify the routes you want to handle.

**Note**: if you don't configure any route it will respond with `404 Not Found` with an empty body.

```php
<?php
declare(strict_types = 1);

require 'path/to/composer/autoload.php';

use Innmind\Framework\{
Main\Http,
Application,
Http\Routes,
};
use Innmind\Router\{
Route,
Route\Variables,
};
use Innmind\Http\Message\{
ServerRequest,
Response\Response,
StatusCode,
};
use Innmind\Filesystem\File\Content;

new class extends Http {
protected function configure(Application $app): Application
{
return $app->appendRoutes(
static fn(Routes $routes) => $routes
->add(Route::literal('GET /')->handle(
static fn(ServerRequest $request) => new Response(
StatusCode::ok,
$request->protocolVersion(),
null,
Content\Lines::ofContent('Hello world!'),
),
))
->add(Route::literal('GET /{name}')->handle(
static fn(ServerRequest $request, Variables $variables) => new Response(
StatusCode::ok,
$request->protocolVersion(),
null,
Content\Lines::ofContent("Hello {$variables->get('name')}!"),
),
)),
);
}
};
```

You can run this script via `cd public && php -S localhost:8080`. If you open your web browser it will display `Hello world!` and if you go to `/John` it will display `Hello John!`.

### Cli

The entrypoint of your cli tools will look something like this.

**Note**: by default if you don't configure any command it will always display `Hello world`.

```php
<?php
declare(strict_types = 1);

require 'path/to/composer/autoload.php';

use Innmind\Framework\{
Main\Cli,
Application,
};
use Innmind\OperatingSystem\OperatingSystem;
use Innmind\TimeContinuum\{
Clock,
Earth\Format\ISO8601,
};
use Innmind\DI\Container;
use Innmind\CLI\{
Console,
Command,
};
use Innmind\Immutable\Str;

new class extends Cli {
protected function configure(Application $app): Application
{
return $app->command(
static fn(Container $container, OperatingSystem $os) => new class($os->clock()) implements Command {
public function __construct(
private Clock $clock,
) {
}

public function __invoke(Console $console): Console
{
$today = $this->clock->now()->format(new ISO8601);

return $console->output(Str::of("We are the: $today\n"));
}

public function usage(): string
{
return 'today';
}
},
);
}
};
```

We can execute our script via `php filename.php` (or `php filename.php today`) and it would output something like `We are the: 2022-12-30T14:04:50+00:00`.
13 changes: 13 additions & 0 deletions docs/README.md
@@ -0,0 +1,13 @@
# Framework

The philosophy behind this framework is to have a minimalist foundation to be able to build simple apps but can also accomodate for more complex applications through composition (of the configuration, commands, request handlers and more).

Another important design is to expose to you the input to handle and an abstraction of the operating system it runs on so you only need to focus on WHAT your app needs to do and NOT HOW.

These topics will guide you through the simplest cases to more complex ones:
- [Build an HTTP app](http.md)
- [Build a CLI app](cli.md)
- [Services](services.md)
- [Middlewares](middlewares.md)
- [Build an app that runs through HTTP and CLI](http-and-cli.md)
- [Testing](testing.md)
158 changes: 158 additions & 0 deletions docs/cli.md
@@ -0,0 +1,158 @@
# Build a CLI app

The first of any CLI app is to create an `entrypoint.php` that you'll call with the `php` command.

```php
<?php
declare(strict_types = 1);

require 'path/to/composer/autoload.php';

use Innmind\Framework\{
Main\Cli,
Application,
};

new class extends Cli {
protected function configure(Application $app): Application
{
return $app;
}
};
```

By default this application will write `Hello world` when you call `php entrypoint.php`.

## Handle commands

This example reuses the AMQP clients defined in the [services topic](services.php).

```php
use Innmind\Framework\{
Main\Cli,
Application,
};
use Innmind\CLI\{
Console,
Command,
};
use Innmind\DI\Container;
use Innmind\AMQP\{
Client,
Command\Publish,
Command\Get,
Model\Basic\Message,
};
use Innmind\Immutable\Str;

new class extends Cli {
protected function configure(Application $app): Application
{
return $app
->service('producer-client', /* see services topic */)
->service('consumer-client', /* see services topic */)
->command(static fn(Container $container) => new class($container('producer-client')) implements Command {
public function __construct(
private Client $amqp,
) {
}

public function __invoke(Console $console): Console
{
$message = Message::of(Str::of(
$console->arguments()->get('url'),
));

return $this
->client
->with(Publish::one($message)->to('some-exchange'))
->run($console)
->match(
static fn($console) => $console->output(Str::of("Message published\n")),
static fn() => $console->error(Str::of("Something went wrong\n")),
);
}

public function usage(): string
{
return 'publish url';
}
})
->command(static fn(Container $container) => new class($container('consumer-client')) implements Command {
public function __construct(
private Client $amqp,
) {
}

public function __invoke(Console $console): Console
{
return $this
->client
->with(Get::of('some-queue'))
->run($console)
->match(
static fn($console) => $console->output(Str::of("One message pulled from queue\n")),
static fn() => $console->error(Str::of("Something went wrong\n")),
);
}

public function usage(): string
{
return 'consume';
}
});
}
};
```

This example creates 2 commands `publish` (that expect one argument) and `consume`. Each command relies on a service to access the AMQP client.

You can call `php entrypoint.php publish https://github.com` that will call the first command and `php entrypoint.php consume` will call the second one.

## Execute code on any command

Sometimes you want to execute some code on every command. So far your only approach would be to use inheritance on each `Command` but this leads to bloated code.

Fortunately there is better approach: composition of `Command`s.

```php
use Innmind\Framework\{
Main\Cli,
Application,
};
use Innmind\CLI\{
Console,
Command,
};

new class extends Cli {
protected function configure(Application $app): Application
{
return $app
->mapCommand(
static fn(Command $command) => new class($command) implements Command {
public function __construct(
private Command $inner,
) {
}

public function __invoke(Console $console): Console
{
// do something before the real command

return ($this->inner)($console);
}

public function usage(): string
{
return $this->inner->usage();
}
}
)
->service(/* ... */)
->service(/* ... */)
->command(/* ... */)
->command(/* ... */);
}
};
```
78 changes: 78 additions & 0 deletions docs/http-and-cli.md
@@ -0,0 +1,78 @@
# Build an app that runs through HTTP and CLI

If you looked at how to build an [HTTP](http.md) and [CLI](cli.md) app you may have noticed that we always configure the same `Application` class. This is intentional to allow you to configure services once (in a [middleware](middlewares)) and use them in both contexts.

Let's take an imaginary app where you can upload images via HTTP (persists them to the filesystem) and a CLI command that pulls a message from an AMQP queue to build the thumbnail. We would build a middleware that roughly looks like this:

```php
use Innmind\Framework\{
Application,
Middleware,
Http\Routes,
Http\Service,
};
use Innmind\OperatingSystem\OperatingSystem;
use Innmind\DI\Container;
use Innmind\Router\Route;
use Innmind\Url\Path;

final class Kernel implements Middleware
{
public function __invoke(Application $app): Application
{
return $app
->service(
'images',
static fn($_, OperatingSystem $os) => $os->filesystem()->mount(Path::of('somewhere/on/the/filesystem/')),
)
->service('amqp', /* see services topic */)
->service('upload', static fn(Container $container) => new UploadHandler( // imaginary class
$container('images'),
$container('amqp'),
))
->appendRoutes(
static fn(Routes $routes, Container $container) => $routes->add(
Route::literal('POST /upload')->handle(Service::of($container, 'upload')),
),
)
->command(static fn(Container $container) => new ThumbnailWorker( // imaginary class
$container('images'),
$container('amqp'),
));
}
}
```

Then you can use this middleware like this:

```php
use Innmind\Framework\{
Main\Cli,
Application,
};

new class extends Cli {
protected function configure(Application $app): Application
{
return $app->map(new Kernel);
}
}
```

Or like this:

```php
use Innmind\Framework\{
Main\Http,
Application,
};

new class extends Http {
protected function configure(Application $app): Application
{
return $app->map(new Kernel);
}
}
```

In the case on the CLI the call to `appendRoutes` will have no effect and for HTTP `command` will have no effect.

0 comments on commit c323caf

Please sign in to comment.