diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..793fb64 --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,27 @@ +name: Documentation +on: + push: + branches: [main] +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 987e2a2..a696500 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ composer.lock vendor +.cache diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6b5c6f1 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +# This command is intended to be run on your computer +serve-doc: + docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material + +build-doc: + docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index c2fa23d..0000000 --- a/docs/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# 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) -- [Add variables to the environment](environment.md) -- [Decorate the operating system](operating-system.md) - -Experimental features: -- [Using an async http server](experimental/async-server.md) diff --git a/docs/assets/favicon.png b/docs/assets/favicon.png new file mode 100644 index 0000000..08dee3a Binary files /dev/null and b/docs/assets/favicon.png differ diff --git a/docs/assets/fonts/MonaspaceNeon-Regular.woff b/docs/assets/fonts/MonaspaceNeon-Regular.woff new file mode 100644 index 0000000..ce0168b Binary files /dev/null and b/docs/assets/fonts/MonaspaceNeon-Regular.woff differ diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg new file mode 100644 index 0000000..6a5d322 --- /dev/null +++ b/docs/assets/logo.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/stylesheets/extra.css b/docs/assets/stylesheets/extra.css new file mode 100644 index 0000000..e4aa2d4 --- /dev/null +++ b/docs/assets/stylesheets/extra.css @@ -0,0 +1,113 @@ +@font-face { + font-family: "Monaspace Neon"; + font-weight: normal; + font-style: normal; + src: url("../fonts/MonaspaceNeon-Regular.woff"); +} + +:root { + --md-code-font: "Monaspace Neon"; +} + +:root { + --light-md-code-hl-number-color: #f76d47; + --light-md-code-hl-function-color: #6384b9; + --light-md-code-hl-operator-color: #39adb5; + --light-md-code-hl-constant-color: #7c4dff; + --light-md-code-hl-string-color: #9fc06f; + --light-md-code-hl-punctuation-color: #39adb5; + --light-md-code-hl-keyword-color: #7c4dff; + --light-md-code-hl-variable-color: #80cbc4; + --light-md-code-hl-comment-color: #ccd7da; + --light-md-code-bg-color: #fafafa; + --light-md-code-fg-color: #ffb62c; + --light-md-code-hl-variable-color: #6384b9; + --dark-md-code-hl-number-color: #f78c6c; + --dark-md-code-hl-function-color: #82aaff; + --dark-md-code-hl-operator-color: #89ddff; + --dark-md-code-hl-constant-color: #c792ea; + --dark-md-code-hl-string-color: #c3e88d; + --dark-md-code-hl-punctuation-color: #89ddff; + --dark-md-code-hl-keyword-color: #c792ea; + --dark-md-code-hl-variable-color: #e8f9f9; + --dark-md-code-hl-comment-color: #546e7a; + --dark-md-code-bg-color: #263238; + --dark-md-code-fg-color: #ffcb6b; + --dark-md-code-hl-variable-color: #82aaff; +} + +@media (prefers-color-scheme: light) { + .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php > * { + --md-code-hl-number-color: var(--light-md-code-hl-number-color); + --md-code-hl-function-color: var(--light-md-code-hl-function-color); + --md-code-hl-operator-color: var(--light-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--light-md-code-hl-constant-color); + --md-code-hl-string-color: var(--light-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--light-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--light-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--light-md-code-hl-comment-color); + --md-code-bg-color: var(--light-md-code-bg-color); + --md-code-fg-color: var(--light-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: light)"] .language-php .na { + --md-code-hl-variable-color: var(--light-md-code-hl-variable-color); +} + +@media (prefers-color-scheme: dark) { + .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); + } + + .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + } +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php > * { + --md-code-hl-number-color: var(--dark-md-code-hl-number-color); + --md-code-hl-function-color: var(--dark-md-code-hl-function-color); + --md-code-hl-operator-color: var(--dark-md-code-hl-operator-color); + --md-code-hl-constant-color: var(--dark-md-code-hl-constant-color); + --md-code-hl-string-color: var(--dark-md-code-hl-string-color); + --md-code-hl-punctuation-color: var(--dark-md-code-hl-punctuation-color); + --md-code-hl-keyword-color: var(--dark-md-code-hl-keyword-color); + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); + --md-code-hl-comment-color: var(--dark-md-code-hl-comment-color); + --md-code-bg-color: var(--dark-md-code-bg-color); + --md-code-fg-color: var(--dark-md-code-fg-color); +} + +[data-md-color-media="(prefers-color-scheme: dark)"] .language-php .na { + --md-code-hl-variable-color: var(--dark-md-code-hl-variable-color); +} diff --git a/docs/cli.md b/docs/cli.md index fcf02b0..40f61bf 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -2,7 +2,7 @@ The first of any CLI app is to create an `entrypoint.php` that you'll call with the `php` command. -```php +```php title="entrypoint.php" 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")), + static fn($console) => $console->output( + Str::of("Message published\n"), + ), + static fn() => $console->error( + Str::of("Something went wrong\n"), + ), ); } @@ -91,8 +95,12 @@ new class extends Cli { ->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")), + static fn($console) => $console->output( + Str::of("One message pulled from queue\n"), + ), + static fn() => $console->error( + Str::of("Something went wrong\n"), + ), ); } diff --git a/docs/environment.md b/docs/environment.md index 7a322d2..03b1a25 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -1,6 +1,6 @@ # Add variables to the environment -The framework exposes an [`Environment`](../src/Environment.php) object with values coming from `\getenv()`. If you want to add variables and make them available to the rest of your application you can do it like this: +The framework exposes an [`Environment`](https://github.com/Innmind/framework/blob/develop/src/Environment.php) object with values coming from `\getenv()`. If you want to add variables and make them available to the rest of your application you can do it like this: ```php use Innmind\Framework\{ @@ -13,10 +13,12 @@ use Innmind\Framework\{ new class extends Http|Cli { protected function configure(Application $app): Application { - return $app->mapEnvironment(static fn(Environment $env) => $env->with( - 'MY_VARIABLE_NAME', - "and it's value", - )); + return $app->mapEnvironment( + static fn(Environment $env) => $env->with( + 'MY_VARIABLE_NAME', + "and it's value", + ), + ); } }; ``` diff --git a/docs/experimental/async-server.md b/docs/experimental/async-server.md index 65e690b..5258470 100644 --- a/docs/experimental/async-server.md +++ b/docs/experimental/async-server.md @@ -2,8 +2,8 @@ The framework comes with an HTTP server entirely built in PHP allowing you to serve your app without extra dependencies in ther earlist stages of your project. -> [!NOTE] -> This feature is optional, to use it you must before run `composer require innmind/async-http-server`. +!!! note "" + This feature is optional, to use it you must before run `composer require innmind/async-http-server`. To use it is similar to the standard [http](../http.md) handler, the first difference is the namespace of the main entrypoint: @@ -30,8 +30,8 @@ Note the namespace is `Main\Async\Http` instead of `Main\Http`. The other differ All the configuration of the `Application` object is identical to the other contexts. -> [!NOTE] ->The server currently does have limitations, streamed requests (via `Transfer-Encoding`) are not supported and multipart requests are not parsed. +!!! note "" + The server currently does have limitations, streamed requests (via `Transfer-Encoding`) are not supported and multipart requests are not parsed. -> [!WARNING] -> This server was built to showcase in a conference talk the ability to switch between synchronous code and asynchronous code without changing the app code. Do **NOT** use this server in production. +!!! warning "" + This server was built to showcase in a conference talk the ability to switch between synchronous code and asynchronous code without changing the app code. Do **NOT** use this server in production. diff --git a/docs/http-and-cli.md b/docs/http-and-cli.md index 091efa0..eb2c17b 100644 --- a/docs/http-and-cli.md +++ b/docs/http-and-cli.md @@ -23,10 +23,12 @@ final class Kernel implements Middleware return $app ->service( 'images', - static fn($_, OperatingSystem $os) => $os->filesystem()->mount(Path::of('somewhere/on/the/filesystem/')), + 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 + ->service('upload', static fn(Container $container) => new UploadHandler( //(1) $container('images'), $container('amqp'), )) @@ -35,7 +37,7 @@ final class Kernel implements Middleware Route::literal('POST /upload')->handle(Service::of($container, 'upload')), ), ) - ->command(static fn(Container $container) => new ThumbnailWorker( // imaginary class + ->command(static fn(Container $container) => new ThumbnailWorker( //(2) $container('images'), $container('amqp'), )); @@ -43,6 +45,9 @@ final class Kernel implements Middleware } ``` +1. imaginary class +2. imaginary class + Then you can use this middleware like this: ```php diff --git a/docs/http.md b/docs/http.md index e65878a..cfa86ed 100644 --- a/docs/http.md +++ b/docs/http.md @@ -80,7 +80,10 @@ new class extends Http { ), )) ->add(Route::literal('GET /{name}')->handle( - static fn(ServerRequest $request, Variables $variables) => Response::of( + static fn( + ServerRequest $request, + Variables $variables, + ) => Response::of( StatusCode::ok, $request->protocolVersion(), null, @@ -163,18 +166,27 @@ new class extends Http { protected function configure(Application $app): Application { return $app - ->route('GET /', static fn(ServerRequest $request) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString('Hello world!'), - )) - ->route('GET /{name}', static fn(ServerRequest $request, Variables $variables) => Response::of( - StatusCode::ok, - $request->protocolVersion(), - null, - Content::ofString("Hello {$variables->get('name')}!"), - )); + ->route( + 'GET /', + static fn(ServerRequest $request) => Response::of( + StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString('Hello world!'), + ), + ) + ->route( + 'GET /{name}', + static fn( + ServerRequest $request, + Variables $variables, + ) => Response::of( + StatusCode::ok, + $request->protocolVersion(), + null, + Content::ofString("Hello {$variables->get('name')}!"), + ), + ); } }; ``` @@ -224,8 +236,10 @@ new class extends Http { ->service( 'hello-name', static fn() => new class { - public function __invoke(ServerRequest $request, Variables $variables): Response - { + public function __invoke( + ServerRequest $request, + Variables $variables, + ): Response { return Response::of( StatusCode::ok, $request->protocolVersion(), @@ -236,8 +250,11 @@ new class extends Http { } ) ->appendRoutes( - static fn(Routes $routes, Container $container) => $routes - ->add(Route::literal('GET /')->handle(Service::of($container, 'hello-word'))), + static fn(Routes $routes, Container $container) => $routes->add( + Route::literal('GET /')->handle( + Service::of($container, 'hello-word'), + ), + ), ) ->route('GET /{name}', To::service('hello-name')); } @@ -246,8 +263,8 @@ new class extends Http { Here the services are invokable anonymous classes to conform to the callable expected for a `Route` but you can create dedicated classes for each one. -> [!NOTE] -> Head to the [services topic](services.md) for a more in-depth look of what's possible. +!!! note "" + Head to the [services topic](services.md) for a more in-depth look of what's possible. ## Executing code on any route @@ -303,8 +320,8 @@ This example will refuse any request that doesn't have an `Authorization` header You can have multiple calls to `mapRequestHandler` to compose behaviours like an onion. -> [!NOTE] -> the default request handler is the inner router of the framework, this means that you can completely change the default behaviour of the framework by returning a new request handler that never uses the default one. +!!! note "" + The default request handler is the inner router of the framework, this means that you can completely change the default behaviour of the framework by returning a new request handler that never uses the default one. ## Handling unknown routes @@ -330,9 +347,11 @@ new class extends Http { StatusCode::notFound, $request->protocolVersion(), null, - Content::ofString('Page Not Found!'), // or return something more elaborated such as html + Content::ofString('Page Not Found!'), //(1) ), ); } }; ``` + +1. or return something more elaborated such as html diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..eb4f8e4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,11 @@ +--- +hide: + - navigation + - toc +--- + +# 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_. diff --git a/docs/middlewares.md b/docs/middlewares.md index 3f3ed92..b88417e 100644 --- a/docs/middlewares.md +++ b/docs/middlewares.md @@ -2,8 +2,8 @@ Middlewares are a way to regroup all the configuration you've seen in other topics under a name. This means that you can either group part of your own application undeer a middleware or expose a package for other to use via Packagist. -> [!NOTE] -> you can search for [`innmind/framework-middlewares` on Packagist](https://packagist.org/providers/innmind/framework-middlewares) for middlewares published by others. +!!! note "" + You can search for [`innmind/framework-middlewares` on Packagist](https://packagist.org/providers/innmind/framework-middlewares) for middlewares published by others. Let's say you have an application that sends emails you could have a middleware that looks like this: @@ -31,16 +31,18 @@ final class Emails implements Middleware return $app ->service( 'email-server' - static fn($_, $__, Environment $env) => Url::of($env->get('EMAIL_SERVER')) + static fn($_, $__, Environment $env) => Url::of( + $env->get('EMAIL_SERVER'), + ), ), ->service( $this->service, - static fn(Container $container) => new EmailClient( // imaginary class + static fn(Container $container) => new EmailClient( //(1) $container('email-server'), ), ) ->command( - static fn(Container $container) => new class($container($this->service)) implements Command { + fn(Container $container) => new class($container($this->service)) implements Command { public function __construct( private EmailClient $client, ) { @@ -63,6 +65,8 @@ final class Emails implements Middleware } ``` +1. imaginary class + And you would use it like this: ```php diff --git a/docs/services.md b/docs/services.md index 5332718..d5be1a5 100644 --- a/docs/services.md +++ b/docs/services.md @@ -47,8 +47,8 @@ enum Services implements Service } ``` -> [!TIP] -> If you publish a package you can add an `@internal` flag on the static methods to tell your users to not use the service. And when you plan to remove a service you can use the `@deprecated` flag. +!!! tip "" + If you publish a package you can add an `@internal` flag on the static methods to tell your users to not use the service. And when you plan to remove a service you can use the `@deprecated` flag. ```php use Innmind\Framework\{ @@ -79,8 +79,8 @@ new class extends Http|Cli { This example defines a single service named `amqpClient` that relies on the `OperatingSystem` in order to work. -> [!NOTE] -> this example uses [`innmind/amqp`](https://github.com/innmind/amqp) +!!! note "" + This example uses [`innmind/amqp`](https://github.com/innmind/amqp). ## Configure via environment variables @@ -104,10 +104,14 @@ new class extends Http|Cli { { return $app->service( Services::amqpClient, - static fn($_, OperatingSystem $os, Environment $env) => Factory::of($os)->make( + static fn( + $_, + OperatingSystem $os, + Environment $env + ) => Factory::of($os)->make( Transport::tcp(), - Url::of($env->get('AMQP_URL')), // this will throw if the variable is not defined - ElapsedPeriod::of($env->maybe('AMQP_TIMEOUT')->match( // in case the variable is not defined it will fallback to a 1000ms timeout + Url::of($env->get('AMQP_URL')), //(1) + ElapsedPeriod::of($env->maybe('AMQP_TIMEOUT')->match( //(2) static fn($timeout) => (int) $timeout, static fn() => 1000, )), @@ -117,6 +121,9 @@ new class extends Http|Cli { }; ``` +1. this will throw if the variable is not defined +2. in case the variable is not defined it will fallback to a `1000ms` timeout + ## Services relying on services If we continue with our AMQP example, in an application that both produces and consumes messages (with high throughput) we'll want 2 different clients. One basic client (like previous examples) for producing messages and one client where we configure the number of messages to prefetch. diff --git a/docs/testing.md b/docs/testing.md index ec57dee..c6d27d1 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -61,7 +61,9 @@ final class AppTest extends TestCase ['AMQP_URL', 'amqp://guest:guest@localhost:5672/'], ]; $os = Factory::build(); // or use mocks - $app = Application::cli($os, Environment::test($variables))->map(new Kernel); + $app = Application::cli($os, Environment::test($variables))->map( + new Kernel, + ); $environment = $app->run(InMemory::of( [], // input chunks diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..a8dbb2b --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,101 @@ +site_name: Innmind/Framework +repo_name: Innmind/Framework + +nav: + - Getting Started: index.md + - Use cases: + - 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 + - Add variables to the environment: environment.md + - Decorate the operating system: operating-system.md + - Experimental features: + - Using an async http server: experimental/async-server.md + +theme: + name: material + logo: assets/logo.svg + favicon: assets/favicon.png + font: false + features: + - content.code.copy + - content.code.annotate + - navigation.tracking + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.expand + - navigation.indexes + - navigation.top + - navigation.footer + - search.suggest + - search.highlight + - content.action.edit + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to light mode + primary: blue + accent: deep orange + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + primary: blue + accent: deep orange + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to system preference + primary: blue + accent: deep orange + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + extend_pygments_lang: + - name: php + lang: php + options: + startinline: true + - pymdownx.inlinehilite + - pymdownx.snippets + - attr_list + - md_in_html + - pymdownx.superfences + - abbr + - admonition + - pymdownx.details: + - pymdownx.tabbed: + alternate_style: true + - toc: + permalink: true + - footnotes + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + +extra_css: + - assets/stylesheets/extra.css + +plugins: + - search + - privacy + +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/Innmind/immutable + - icon: fontawesome/brands/x-twitter + link: https://twitter.com/Baptouuuu