diff --git a/.gitignore b/.gitignore index 4fbb073..e26c29e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ -/vendor/ +/build/ /composer.lock +/vendor/ diff --git a/README.md b/README.md index 29c1f08..042ae05 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FrugalPHP +# Frugal [![CI status](https://github.com/clue/frugalphp-incubator/workflows/CI/badge.svg)](https://github.com/clue/frugalphp-incubator/actions) @@ -10,21 +10,7 @@ Lightweight microframework for fast, event-driven and async-first web applicatio Take a look at https://ninenines.eu/docs/en/cowboy/2.6/guide/modern_web/ * [Quickstart](#quickstart) -* [Basics](#basics) - * [Installation](#installation) - * [Structure your app (Controllers)](#structure-your-app-controllers) - * [Testing your app](#testing-your-app) - * [Deployment](#deployment) -* [Usage](#usage) - * [App](#app) - * [Request](#request) - * [Response](#response) - * [Database](#database) - * [Filesystem](#filesystem) - * [Authentication](#authentication) - * [Sessions](#sessions) - * [Templates](#templates) - * [Queuing](#queuing) +* [Documentation](#documentation) * [Tests](#tests) * [License](#license) @@ -38,7 +24,7 @@ First manually change your `composer.json` to include these lines: "repositories": [ { "type": "vcs", - "url": "https://github.com/clue/frugalphp" + "url": "https://github.com/clue-engineering/frugal" } ] } @@ -49,7 +35,7 @@ First manually change your `composer.json` to include these lines: Simply install FrugalPHP: ```bash -$ composer require clue/frugal:dev-master +$ composer require clue/frugal:dev-main ``` > TODO: Tagged release. @@ -98,232 +84,34 @@ HTTP/1.1 200 OK Hello wörld! ``` -## Basics - -### Installation - -* Runs everywhere -* Requires only PHP 7.1+, no extensions required -* Can run behind existing web servers or locally with built-in webserver (see deployment) - -[…] - -### Structure your app (Controllers) - -Once everything is up and running, we can take a look at how to best structure -our actual web application. - -To get started, it's often easiest to start with simple closure definitions -like the following: - -```php -get('/', function () { - return new React\Http\Message\Response( - 200, - [], - "Hello wörld!\n" - ); -}); - -$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) { - return new React\Http\Message\Response( - 200, - [], - "Hello " . $request->getAttribute('name') . "!\n" - ); -}); - -$loop->run(); -``` - -While easy to get started, this will easily get out of hand for more complex -business domains when you have more than a couple of routes registered. - -For real-world applications, we highly recommend structuring your application -into invidividual controller classes. This way, we can break up the above -definition into three even simpler files: - -```php -# main.php -get('/', new Acme\Todo\HelloController()); -$app->get('/users/{name}', new Acme\Todo\UserController()); - -$loop->run(); -``` - -```php -# src/HelloController.php -getAttribute('name') . "!\n" - ); - } -} -``` - -Doesn't look too complex, right? Now, we only need to tell Composer's autoloader -about our vendor namespace `Acme\\Todo` in the `src/` folder. Make sure to include -the following lines in your `composer.json` file: - -```json -{ - "autoload": { - "psr-4": { - "Acme\\Todo\\": "src/" - } - } -} -``` - -When we're doing this the first time, we have to update Composer's generated -autoloader classes: - -```bash -$ composer dump-autoload -``` - -Don't worry, that's a one-time setup only. If you're used to working with -Composer, this shouldn't be too surprising. If this sounds new to you, rest -assured this is the only time you have to worry about this, new classes can -simply be added without having to run Composer again. - -Again, let's see our web application still works by using your favorite -webbrowser or command line tool: - -```bash -$ curl -v http://localhost:8080/ -HTTP/1.1 200 OK -… - -Hello wörld! -``` - -### Testing your app - -**We ❤️ TDD and DDD!** - -New to testing your web application? While we don't want to *force* you to test -your app, we want to emphasize the importance of automated test suits and try hard -to make testing your web application as easy as possible. - -Once your app is structured into dedicated controller classes as per the previous -chapter, […] - -> TODO: PHPUnit setup basics and first test cases. - -> TODO: Higher-level functional tests. - -### Deployment - -Runs everywhere: - -* Built-in webserver -* nginx with PHP-FPM -* Apache with mod_fcgid and PHP-FPM -* Apache with mod_php -* PHP's development webserver - -[…] - -## Usage - -### App - -* Batteries included, but swappable -* Providing HTTP routing (RESTful applications) - -### Request - -* PSR-7 - -### Response - -* PSR-7 - -### Database - -* Async -* No PDO, no Doctrine and family -* Easy to spot, harder to replace -* MySQL, Postgres and SQLite supported -* ORM -* Redis - -### Filesystem - -* Async -* No `fopen()`, `file_get_contents()` and family -* Easy to overlook -* Few blocking calls *can* be acceptable - -### Authentication - -* Basic auth easy -* HTTP middleware better -* JWT and oauth possible - -### Sessions - -* Built-in (or module?) -* HTTP middleware -* Persistence via database/ORM or other mechanism? - -### Templates - -* Any template language possible -* Twig recommended? - -### Queuing - -* Built-in (or module?) -* Redis built-in, but swappable with real instance (constraints?) +## Documentation + +Hooked? +See [full documentation](docs/) for more details. + +> We use [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) to +> render our documentation to a pretty HTML version. +> +> If you want to contribute to the documentation, it's easiest to just run +> this in a Docker container like this: +> +> ```bash +> $ docker run --rm -it -p 8000:8000 -v ${PWD}:/docs squidfunk/mkdocs-material +> ``` +> +> You can access the documentation via `http://localhost:8000`. +> If you want to generate a static HTML folder for deployment, you can again +> use a Docker container like this: +> +> ```bash +> $ docker run --rm -it -v ${PWD}:/docs squidfunk/mkdocs-material build +> ``` +> +> The resulting `build/docs/` should then be deployed behind a web server (tbd). +> If you want to add a new documentation file and/or change the page order, make sure the [`mkdocs.yml`](mkdocs.yml) +> file contains an up-to-date list of all pages. +> +> Happy hacking! ## Tests @@ -342,7 +130,7 @@ $ php vendor/bin/phpunit Additionally, you can run some simple acceptance tests to verify the framework examples work as expected behind your web server. Use your web server of choice -(see [deployment](#deployment)) and execute the tests with the URL to your +(see deployment documentation) and execute the tests with the URL to your installation like this: ```bash diff --git a/docs/api/app.md b/docs/api/app.md new file mode 100644 index 0000000..9a4826d --- /dev/null +++ b/docs/api/app.md @@ -0,0 +1,153 @@ +# App + +The `App` class is your main entrypoint to any application that builds on top of X. +It provides a simple API for routing HTTP requests as commonly used in RESTful applications. + +Internally, the `App` object builds on top of [ReactPHP](https://reactphp.org/) +to do its magic, hence you have to create it like this: + +```php +run(); +$loop->run(); +``` + +> ℹ️ **Heads up!** +> +> Major improvements upcoming! We're actively contributing to our underlying +> libraries to make sure this can look like this in the near future: +> +> ```php +> +> require __DIR__ . '/vendor/autoload.php'; +> +> $app = 🚀🚀🚀\App(); +> +> // Register routes here, see routing… +> +> $app->run(); +> ``` + +## Routing + +The `App` class offers a number of API methods that allow you to route incoming +HTTP requests to controller functions. In its most simple form, you can add +multiple routes using inline closures like this: + +```php +$app->get('/user', function () { + return new React\Http\Message\Response(200, [], "hello everybody!"); +}); + +$app->get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { + $id = $request->getAttribute('id'); + return new React\Http\Message\Response(200, [], "hello $id"); +}); +``` + +For example, an HTTP `GET` request for `/user` would call the first controller +function. +An HTTP `GET` request for `/user/alice` would call the second controller function +which also highlights how you can use [request attributes](request.md#attributes) +to access values from URI templates. + +An HTTP `GET` request for `/foo` would automatically reject the HTTP request with +a 404 (Not Found) error response unless this route is registered. +Likewise, an HTTP `POST` request for `/user` would reject with a 405 (Method Not +Allowed) error response unless a route for this method is also registered. + +You can route any number of incoming HTTP requests to controller functions by +using the matching API methods like this: + +```php +$app->get('/user/{id}', $controller); +$app->head('/user/{id}', $controller); +$app->post('/user/{id}', $controller); +$app->put('/user/{id}', $controller); +$app->patch('/user/{id}', $controller); +$app->delete('/user/{id}', $controller); +$app->options('/user/{id}', $controller); +``` + +If you want to map multiple HTTP request methods to a single controller, you can +use this shortcut instead of listing each method explicitly like above: + +``` +$app->map(['GET', 'POST'], '/user/{id}', $controller); +``` + +If you want to map each and every HTTP request method to a single controller, +you can use this additional shortcut: + +``` +$app->any('/user/{id}', $controller); +``` + +## Controllers + +The above examples use inline closures as controller functions to make these +examples more concise: + +``` +$app->get('/', function () { + return new React\Http\Message\Response( + 200, + [], + "Hello wörld!\n" + ); +}); +``` + +While easy to get started, it's easy to see how this would become a mess once +you keep adding more controllers to a single application. +For this reason, we recommend using [controller classes](../best-practices/controllers.md) +for production use-cases like this: + +```php +# main.php +$app->get('/', new Acme\Todo\HelloController()); +``` + +```php +# src/HelloController.php + ℹ️ **Feature preview** +> +> This is a feature preview, i.e. it might not have made it into the current beta. +> Give feedback to help us prioritize. + +One of the main features of the `App` is middleware support. +Middleware allows you to extract common functionality such as HTTP login, session handling or logging into reusable components. +These middleware components can be added to both individual routes or globally to all registered routes. +See [middleware documentation](../04-middleware.md) for more details. diff --git a/docs/api/middleware.md b/docs/api/middleware.md new file mode 100644 index 0000000..e7a4b53 --- /dev/null +++ b/docs/api/middleware.md @@ -0,0 +1,88 @@ +# Middleware + +> ℹ️ **Feature preview** +> +> This is a feature preview, i.e. it might not have made it into the current beta. +> Give feedback to help us prioritize. + +One of the main features of X is middleware support. +Middleware allows you to extract common functionality such as an HTTP login, session handling or logging into reusable components. + +To get started, we can add an example middleware handler to an individual route +by adding an additional callable before the final controller like this: + +```php hl_lines="3-6" +$app->get( + '/user', + function (Psr\Http\Message\ServerRequestInterface $request, callable $next) { + $request = $request->withAttribute('admin', false); + return $next($request); + }, + function (Psr\Http\Message\ServerRequestInterface $request) { + $role = $request->getAttribute('admin') ? 'admin' : 'user'; + return new React\Http\Message\Response(200, [], "hello $role!"); + } +); +``` + +For example, an HTTP `GET` request for `/user` would first call the middleware handler which then modifies this request and passes the modified request to the next controller function. + +While easy to get started, it's easy to see how this would become a mess once you +keep adding more controllers to a single application. +For this reason, we recommend using middleware classes for production use-cases +like this: + +```php hl_lines="8" +# main.php + +use Acme\Todo\AdminMiddleware; +use Acme\Todo\UserController; + +// … + +$app->get('/user', new AdminMiddleware(), new UserController()); +``` + +```php +# src/AdminMiddleware.php +withAttribute('admin', false); + return $next($request); + } +} +``` + +Likewise, you can add any number of middleware handlers to each route. +Each middleware is responsible for calling the next handler in the chain or +directly returning an error response if the request should not be processed. + +Additionally, you can also add middleware to the `App` object itself to register +a global middleware handler for all registered routes: + +```php hl_lines="7" +get('/user', new UserController()); + +$app->run(); +$loop->run(); +``` + +You can also combine global middleware handlers (think logging) with additional +middleware handlers for individual routes (think authentication). +Global middleware handlers will always be called before route middleware handlers. diff --git a/docs/api/request.md b/docs/api/request.md new file mode 100644 index 0000000..033b688 --- /dev/null +++ b/docs/api/request.md @@ -0,0 +1,201 @@ +# Request + +Whenever the client sends an HTTP request to our application, +we receive this as an request object and need to react to it. + +We love standards and want to make using X as simple as possible. +That's why we build on top of the established [PSR-7 standard](https://www.php-fig.org/psr/psr-7/) +(HTTP message interfaces). +This standard defines common interfaces for HTTP request and response objects. + +If you've ever used PSR-7 before, you should immediately feel at home when using X. +If you're new to PSR-7, don't worry. +Here's everything you need to know to get started. + +> ℹ️ **A note about other PSR-7 implementations** +> +> This documentation uses the +> [`Psr\Http\Message\ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface) +> for all examples. +> The actual class implementing this interface is an implementation detail that +> should not be relied upon. +> If you need to construct your own instance, we recommend using the +> [`React\Http\Message\ServerRequest`](https://reactphp.org/http/#serverrequest) +> class because this comes bundled as part of our dependencies, +> but you may use any other implementation as long as +> it implements the same interface. + +## Attributes + +You can access request attributes like this: + +```php +$app->get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { + $id = $request->getAttribute('id'); + + return new React\Http\Message\Response(200, [], "Hello $id"); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user/Alice +Hello Alice +``` + +These custom attributes are most commonly used when using URI placeholders +from [routing](app.md#routing). +Each placeholder will automatically be assigned to a matching request attribute. +See also [routing](app.md#routing) for more details. + +Additionally, these custom attributes can also be useful when passing additional +information from a middleware handler to other handlers further down the chain +(think authentication information). +See also [middleware](middleware.md) for more details. + +## JSON + +You can access JSON data from the HTTP request body like this: + +```php +$app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) { + $data = json_decode((string) $request->getBody()); + $name = $data->name ?? 'anonymous'; + + return new React\Http\Message\Response(200, [], "Hello $name"); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user --data '{"name":"Alice"}' +Hello Alice +``` + +Additionally, you may want to validate the `Content-Type: application/json` request header +to be sure the client intended to send a JSON request body. + +This example returns a simple text response, you may also want to return a +[JSON response](response.md#json) for common API usage. + +## Form data + +You can access HTML form data from the HTTP request body like this: + +```php +$app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) { + $data = $request->getParsedBody(); + $name = $data['name'] ?? 'Anonymous'; + + return new React\Http\Message\Response(200, [], "Hello $name"); +}); +``` + + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user -d name=Alice +Hello Alice +``` + +This method returns a possibly nested array of form fields, very similar to +PHP's `$_POST` superglobal. + +## Uploads + +You can access any file uploads from HTML forms like this: + +```php +$app->post('/user', function (Psr\Http\Message\ServerRequestInterface $request) { + $files = $request->getUploadedFiles(); + $name = isset($files['image']) ? $files['image']->getClientFilename() : 'x'; + + return new React\Http\Message\Response(200, [], "Uploaded $name"); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user -F image=@~/Downloads/image.jpg +Uploaded image.jpg +``` + +This method returns a possibly nested array of files uploaded, very similar +to PHP's `$_FILES` superglobal. +Each file in this array implements the `Psr\Http\Message\UploadedFileInterface`: + +```php +$files = $request->getUploadedFiles(); +$image = $files['image']; +assert($image instanceof Psr\Http\Message\UploadedFileInterface); + +$stream = $image->getStream(); +assert($stream instanceof Psr\Http\Message\StreamInterface); +$contents = (string) $stream; + +$size = $image->getSize(); +assert(is_int($size)); + +$name = $image->getClientFilename(); +assert(is_string($name) || $name === null); + +$type = $image->getClientMediaType(); +assert(is_string($type) || $name === null); +``` + +> ℹ️ **Info** +> +> Note that HTTP requests are currently limited to 64 KiB. Any uploads above +> this size will currently show up as an empty request body with no file uploads +> whatsoever. + +## Headers + +You can access all HTTP request headers like this: + +```php +$app->get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { + $agent = $request->getHeaderLine('User-Agent'); + + return new React\Http\Message\Response(200, [], "Hello $agent"); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user -H 'User-Agent: FrameworkX/0' +Hello FrameworkX/0 +``` + +This example returns a simple text response with no additional response headers, +you may also want to return [response headers](response.md#headers) for common API usage. + +## Parameters + +You can access server-side parameters like this: + +```php +$app->get('/user', function (Psr\Http\Message\ServerRequestInterface $request) { + $params = $request->getServerParams(); + $ip = $params['REMOTE_ADDR'] ?? 'unknown'; + + return new React\Http\Message\Response(200, [], "Hello $ip"); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user +Hello 127.0.0.1 +``` + +This method returns an array of server-side parameters, very similar +to PHP's `$_SERVER` superglobal. +Note that available server parameters depend on the server software and version +in use. diff --git a/docs/api/response.md b/docs/api/response.md new file mode 100644 index 0000000..0eaa78a --- /dev/null +++ b/docs/api/response.md @@ -0,0 +1,238 @@ +# Response + +Whenever the client sends an HTTP request to our application, +we need to send back an HTTP response message. + +We love standards and want to make using X as simple as possible. +That's why we build on top of the established [PSR-7 standard](https://www.php-fig.org/psr/psr-7/) +(HTTP message interfaces). +This standard defines common interfaces for HTTP request and response objects. + +If you've ever used PSR-7 before, you should immediately feel at home when using X. +If you're new to PSR-7, don't worry. +Here's everything you need to know to get started. + +> ℹ️ **A note about other PSR-7 implementations** +> +> All of the examples in this documentation use the +> [`React\Http\Message\Response`](https://reactphp.org/http/#response) class +> because this comes bundled as part of our dependencies. +> If you have more specific requirements or want to integrate this with an +> existing piece of code, you can use any response implementation as long as +> it implements the [`Psr\Http\Message\ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface). + +## JSON + +You can send JSON data as an HTTP response body like this: + +```php +$app->get('/user', function () { + $data = [ + [ + 'name' => 'Alice' + ], + [ + 'name' => 'Bob' + ] + ]; + + return new React\Http\Message\Response( + 200, + ['Content-Type' => 'application/json'], + json_encode($data) + ); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user +[{"name":"Alice"},{"name":"Bob"}] +``` + +If you want to return pretty-printed JSON, all you need to do is passing the +correct flags when encoding: + +```php hl_lines="14-17" +$app->get('/user', function () { + $data = [ + [ + 'name' => 'Alice' + ], + [ + 'name' => 'Bob' + ] + ]; + + return new React\Http\Message\Response( + 200, + ['Content-Type' => 'application/json'], + json_encode( + $data, + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ) + ); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user +[ + { + "name": "Alice" + }, + { + "name":"Bob" + } +] +``` + +This example returns a simple JSON response from some static data. +In real-world applications, you may want to load this from a +[database](../integrations/database.md). +For common API usage, you may also want to receive a [JSON request](request.md#json). + +## HTML + +You can send HTML data as an HTTP response body like this: + +```php +$app->get('/user', function () { + $html = <<Hello Alice +HTML; + + return new React\Http\Message\Response( + 200, + ['Content-Type' => 'text/html; charset=utf-8'], + $html + ); +}); +``` + +An HTTP request can be sent like this: + +```bash +$ curl http://localhost:8080/user +

Hello Alice

+``` + +This example returns a simple HTML response from some static data. +In real-world applications, you may want to load this from a +[database](../integrations/database.md) and perhaps use +[templates](../integrations/templates.md) to render your HTML. + +## Status Codes + +You can assign status codes like this: + +```php hl_lines="5 12" +$app->get('/user/{id}', function (Psr\Http\Message\ServerRequestInterface $request) { + $id = $request->getAttribute('id'); + if ($id === 'admin') { + return new React\Http\Message\Response( + 403, + [], + 'Forbidden' + ); + } + + return new React\Http\Message\Response( + 200, + [], + "Hello $id" + ); +}); +``` + +An HTTP request can be sent like this: + +```bash hl_lines="2 6" +$ curl http://localhost:8080/user/Alice -I +HTTP/1.1 200 OK +… + +$ curl http://localhost:8080/user/admin -I +HTTP/1.1 403 Forbidden +… +``` + +Each HTTP response message contains a status code that describes whether the +HTTP request has been successfully completed. +Here's a list of the most common HTTP status codes: + +* 200 (OK) +* 301 (Permanent Redirect) +* 302 (Temporary Redirect) +* 403 (Forbidden) +* 404 (Not Found) +* 500 (Internal Server Error) +* … + +See [list of HTTP status codes](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) for more details. + +## Headers + +You can assign HTTP response headers like this: + +```php hl_lines="4" +$app->get('/user', function () { + return new React\Http\Message\Response( + 200, + ['Content-Type' => 'text/plain; charset=utf-8'], + "Hello $id" + ); +}); +``` + +An HTTP request can be sent like this: + +```bash hl_lines="3" +$ curl http://localhost:8080/user -I +HTTP/1.1 200 OK +Content-Type: text/plain; charset=utf-8 +Server: ReactPHP/1 +Date: Fri, 30 Apr 2021 08:51:04 GMT +Content-Length: 88 +Connection: close +``` + +Each HTTP response message can contain an arbitrary number of response headers. +You can pass these headers as an associative array to the response object. + +Additionally, the application will automatically include default headers required +by the HTTP protocol. +It's not recommended to mess with these default headers unless you're sure you +know what you're doing. + +## Internal Server Error + +Each controller function needs to return a response object in order to send +an HTTP response message. +If the controller functions throws an `Exception` (or `Throwable`) or any other type, the +HTTP request will automatically be rejected with a 500 (Internal Server Error) +HTTP error response: + +```php +$app->get('/user', function () { + // TODO: load data + throw new BadMethodCallException(); +}); +``` + +An HTTP request can be sent like this: + +```bash hl_lines="2" +$ curl http://localhost:8080/user -I +HTTP/1.1 500 Internal Server Error +… +``` + +This error message contains only few details to the client to avoid leaking +internal information. +If you want to implement custom error handling, you're recommended to either +catch any exceptions your own or use a [middleware handler](middleware.md) to +catch any exceptions in your application. diff --git a/docs/async/child-processes.md b/docs/async/child-processes.md new file mode 100644 index 0000000..37ed21a --- /dev/null +++ b/docs/async/child-processes.md @@ -0,0 +1,13 @@ +# Parallel processing with child processes + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Avoid blocking by moving blocking implementation to child process +* Child process I/O for communication +* Multithreading, but isolated processes +* See [reactphp/child-process](https://reactphp.org/child-process/) for underlying APIs +* See [clue/reactphp-pq](https://github.com/clue/reactphp-pq) for higher-level API to automatically wrap blocking functions in an async child process and turn blocking functions into non-blocking [promises](promises.md) diff --git a/docs/async/coroutines.md b/docs/async/coroutines.md new file mode 100644 index 0000000..38acbb0 --- /dev/null +++ b/docs/async/coroutines.md @@ -0,0 +1,15 @@ +# Coroutines + +> ⚠️ **Feature preview** +> +> This is a feature preview, i.e. it might not have made it into the current beta. +> Give feedback to help us prioritize. +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* [Promises](promises.md) can be hard due to nested callbacks +* X provides Generator-based coroutines +* Synchronous code structure, yet asynchronous execution +* Generators can be a bit harder to understand, see [Fibers](fibers.md) for future PHP 8.1 API. diff --git a/docs/async/fibers.md b/docs/async/fibers.md new file mode 100644 index 0000000..1fb5610 --- /dev/null +++ b/docs/async/fibers.md @@ -0,0 +1,18 @@ +# Fibers + +> ⚠️ **Feature preview** +> +> This is a feature preview, i.e. it might not have made it into the current beta. +> Give feedback to help us prioritize. +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Hot topic for PHP 8.1 +* Async APIs that look just like their synchronous counterparts +* Much easier to integrate, possibly larger ecosystem in future +* Requires PHP 8.1 (November 2021), adoption will take time +* [Promises](promises.md) still required for concurrent execution +* [Promises](promises.md) and [Coroutines](coroutines.md) work just fine until ecosystem matures +* See [blog post](https://clue.engineering/2021/fibers-in-php) diff --git a/docs/async/promises.md b/docs/async/promises.md new file mode 100644 index 0000000..c625d30 --- /dev/null +++ b/docs/async/promises.md @@ -0,0 +1,14 @@ +# Promises + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Avoid blocking ([databases](../integrations/database.md), [filesystem](../integrations/filesystem.md), etc.) +* Deferred execution +* Concurrent execution more efficient than [multithreading](child-processes.md) +* API can be a bit harder (see [Coroutines](coroutines.md) or [Fibers](fibers.md)) +* See [reactphp/promise](https://reactphp.org/promise/) +* Avoid blocking by moving blocking implementation to [child process](child-processes.md) diff --git a/docs/async/streaming.md b/docs/async/streaming.md new file mode 100644 index 0000000..6211394 --- /dev/null +++ b/docs/async/streaming.md @@ -0,0 +1,25 @@ +# Streaming + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +Processing large amounts of data or when data arrives at future time. + +## Streaming downloads + +* Efficient processing of large files without keeping contents in memory +* Similar for video streaming, but not scope of this project + +## EventSource + +* HTML5 Server-Sent Events (SSE) aka. EventSource supported out-of-the-box +* Live streaming, live data, realtime communication + +## WebSockets + +* HTML5 WebSockets integration supported with [Ratchet](http://socketo.me/) +* See also EventSource as alternative +* Bidirectional communication diff --git a/docs/best-practices/controllers.md b/docs/best-practices/controllers.md new file mode 100644 index 0000000..25e8167 --- /dev/null +++ b/docs/best-practices/controllers.md @@ -0,0 +1,144 @@ +# Controller classes to structure your app + +When starting with X, it's often easiest to start with simple closure definitions like suggested in the [quickstart guide](../getting-started/quickstart.md). + +As a next step, let's take a look at how this structure can be improved with controller classes. +This is especially useful once you leave the prototyping phase and want to find the best structure for a production-ready setup. + +To get started, let's take a look at the following simple closure definitions: + +```php +# app.php +get('/', function () { + return new React\Http\Message\Response( + 200, + [], + "Hello wörld!\n" + ); +}); + +$app->get('/users/{name}', function (Psr\Http\Message\ServerRequestInterface $request) { + return new React\Http\Message\Response( + 200, + [], + "Hello " . $request->getAttribute('name') . "!\n" + ); +}); + +$app->run(); +$loop->run(); +``` + +While easy to get started, it's also easy to see how this will get out of hand for more complex +business domains when you have more than a couple of routes registered. + +For real-world applications, we highly recommend structuring your application +into invidividual controller classes. This way, we can break up the above +definition into three even simpler files: + +```php +# app.php +get('/', new Acme\Todo\HelloController()); +$app->get('/users/{name}', new Acme\Todo\UserController()); + +$app->run(); +$loop->run(); +``` + +```php +# src/HelloController.php +getAttribute('name') . "!\n" + ); + } +} +``` + +Doesn't look too complex, right? Now, we only need to tell Composer's autoloader +about our vendor namespace `Acme\\Todo` in the `src/` folder. Make sure to include +the following lines in your `composer.json` file: + +```json +{ + "autoload": { + "psr-4": { + "Acme\\Todo\\": "src/" + } + } +} +``` + +When we're doing this the first time, we have to update Composer's generated +autoloader classes: + +```bash +$ composer dump-autoload +``` + +> ℹ️ **New to Composer?** +> +> Don't worry, that's a one-time setup only. If you're used to working with +Composer, this shouldn't be too surprising. If this sounds new to you, rest +assured this is the only time you have to worry about this, new classes can +simply be added without having to run Composer again. + +Again, let's see our web application still works by using your favorite +webbrowser or command line tool: + +```bash +$ curl -v http://localhost:8080/ +HTTP/1.1 200 OK +… + +Hello wörld! +``` + +If everything works as expected, we can continue with writing our first tests to automate this. diff --git a/docs/best-practices/deployment.md b/docs/best-practices/deployment.md new file mode 100644 index 0000000..625aafb --- /dev/null +++ b/docs/best-practices/deployment.md @@ -0,0 +1,85 @@ +# Production deployment + +One of the nice properties of X is that it **runs anywhere**, i.e. it works both +behind traditional web server setups as well as in a stand-alone environment. +This makes it easy to get started with existing web application stacks, yet it +provides even more awesome features with its built-in web server. + +## Traditional stacks + +No matter what existing PHP stack you're using, X runs anywhere. +This means that if you've already used PHP before, X will *just work*. + +* nginx with PHP-FPM +* Apache with PHP-FPM, mod_fcgid, mod_cgi or mod_php +* Any other web server using FastCGI to talk to PHP-FPM +* Linux, Mac and Windows operating systems (LAMP, MAMP, WAMP) + +*We've got you covered.* + +For example, if you've followed the [quickstart guide](../getting-started/quickstart.md), you can run this using PHP's built-in development web +server for testing purposes like this: + +```bash +$ php -S 0.0.0.0:8080 app.php +``` + +In order to check your web application responds as expected, you can use your favorite webbrowser or command line tool: + +```bash +$ curl -v http://localhost:8080/ +HTTP/1.1 200 OK +… + +Hello wörld! +``` + +## Built-in web server + +But there's more! +Framework X ships its own efficient web server implementation written in pure PHP. +This uses an event-driven architecture to allow you to get the most out of Framework X. +With the built-in web server, we provide a non-blocking implementation that can handle thousands of incoming connections and provide a much better user experience in high-load scenarios. + +With no changes required, you can run the built-in web server with the exact same code base on the command line: + +```bash +$ php app.php +``` + +Let's take a look and see this works just like before: + +```bash +$ curl -v http://localhost:8080/ +HTTP/1.1 200 OK +… + +Hello wörld! +``` + +You may be wondering how fast a pure PHP web server implementation could possibly be. + +``` +$ ab -n10000 -c10 http://localhost:8080/ +… +Concurrency Level: 10 +Time taken for tests: 0.991 seconds +Complete requests: 10000 +Failed requests: 0 +Total transferred: 1090000 bytes +HTML transferred: 130000 bytes +Requests per second: 10095.17 [#/sec] (mean) +Time per request: 0.991 [ms] (mean) +Time per request: 0.099 [ms] (mean, across all concurrent requests) +Transfer rate: 1074.58 [Kbytes/sec] received +``` + +The answer: Very fast! + +If you're going to use this in production, we still recommend running this +behind a reverse proxy such as nginx, HAproxy, etc. for TLS termination +(HTTPS support). + +Additionally, you should use service monitoring to make sure the server will +automatically restart after system reboot or failure. Docker containers or +systemd unit files would be common solutions here. diff --git a/docs/best-practices/testing.md b/docs/best-practices/testing.md new file mode 100644 index 0000000..513a628 --- /dev/null +++ b/docs/best-practices/testing.md @@ -0,0 +1,199 @@ +# Testing + +> ℹ️ **New to testing your web application?** +> +> While we don't want to *force* you to test your app, we want to emphasize the +> importance of automated test suites and try hard to make testing your web +> application as easy as possible. +> +> Tests allow you to verify correct behavior of your implementation, so that you +> match expected behavior with the actual implementation. +> And perhaps more importantly, by automating this process you can be sure +> future changes do not introduce any regressions and suddenly break something else. +> *Develop your application with ease and certainty.* +> +> **We ❤️ TDD!** + +## PHPUnit basics + +Once your app is structured into [dedicated controller classes](controllers.md) +as per the previous chapter, we can test each controller class in isolation. +This way, testing becomes pretty straight forward. + +Let's start simple and write some unit tests for our simple `HelloController` class: + +```php +# src/HelloController.php + ℹ️ **New to PHPUnit?** +> +> If you haven't heard about [PHPUnit](https://phpunit.de/) before, +> PHPUnit is *the* testing framework for PHP projects. +> After installing it as a development dependency, we can take advantage of its +> structure to write tests for our own application. + +Next, we can start by creating our first unit test: + +```php +# tests/HelloControllerTest.php +assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals("Hello wörld!\n", (string) $response->getBody()); + } +} +``` + +We're intentionally starting simple. +By starting with a controller class following a somewhat trivial implementation, +we can focus on just getting the test suite up and running first. +All following tests will also follow a somewhat similar structure, so we can +always use this as a simple building block: + +* create an HTTP request object +* pass it into our controller function +* and then run assertions on the expected HTTP response object. + +Once you've created your first unit tests, it's time to run PHPUnit by executing +this command in the project directory: + +``` +$ vendor/bin/phpunit tests +PHPUnit 9.5.4 by Sebastian Bergmann and contributors. + +. 1 / 1 (100%) + +Time: 00:00.006, Memory: 4.00 MB + +OK (1 test, 1 assertion) +``` + +## Testing with specific requests + +Once the basic test setup works, let's continue with testing a controller that +shows different behavior depending on what HTTP request comes in. +For this example, we're using [request attributes](../api/request.md#attributes), +but the same logic applies to testing different URLs, HTTP request headers, etc.: + +```php +# src/UserController.php +getAttribute('name') . "!\n" + ); + } +} +``` + +Again, we create a new test class matching the controller class: + +```php +# tests/UserControllerTest.php +withAttribute('name', 'Alice'); + + $controller = new UserController(); + $response = $controller($request); + + $this->assertInstanceOf(ResponseInterface::class, $response); + $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals("Hello Alice!\n", (string) $response->getBody()); + } +} +``` + +This follows the exact same logic like the previous example, except this time +we're setting up a specific HTTP request and asserting the HTTP response +contains the correct name. +Again, we can run PHPUnit in the project directory to see this works as expected: + +``` +$ vendor/bin/phpunit tests +PHPUnit 9.5.4 by Sebastian Bergmann and contributors. + +.. 2 / 2 (100%) + +Time: 00:00.003, Memory: 4.00 MB + +OK (2 tests, 2 assertions) +``` + +## Further reading + +If you've made it this far, you should have a basic understanding about how +testing can help you *develop your application with ease and certainty*. +We believe mastering TTD is well +worth it, but perhaps this is somewhat out of scope for this documentation. +If you're curious, we recommend looking into the following topics: + +* TDD +* Higher-level functional tests +* Test automation +* CI / CD diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md new file mode 100644 index 0000000..0b2455f --- /dev/null +++ b/docs/getting-started/quickstart.md @@ -0,0 +1,114 @@ +# Quickstart in 5 minutes + +Getting started with X is easy! +Here's a quick tutorial to get you up and running in 5 minutes or less. +Start your timer and here we go! + +## Code + +In order to first start using X, let's start with an entirely empty project directory. +This shouldn't be too confusing, but here's how you can do so on the common line: + +```bash +$ mkdir ~/projects/acme +$ cd ~/projects/acme +``` + +Next, we can start by taking a look at a simple example application. +You can use this example to get started by creating a new `app.php` file in your empty project directory: + +```php +get('/', function () { + return new React\Http\Message\Response( + 200, + [], + "Hello wörld!\n" + ); +}); + +$app->run(); +$loop->run(); +``` + +On a code level, this is everything you need to get started. +For more sophisticated projects, you may want to make sure to [structure your controllers](../best-practices/controllers.md), +but the above should be just fine for starters. + +## Installation + +Next, we need to install X and its dependencies to actually run this project. + +> ⚠️ **Beta** +> +> This project is currently in closed beta, so the installation requires a manual step first. +> This will not be necessary once the project is released to the public. +> +> Start by creating a new `composer.json` in the project directory with the following contents: +> +> ```json +> { +> "repositories": [ +> { +> "type": "vcs", +> "url": "https://github.com/clue-engineering/frugal" +> } +> ] +> } +> ``` + +Thanks to [Composer](https://getcomposer.org/), this installation only requires a single command. + +> ℹ️ **New to Composer?** +> +> If you haven't heard about Composer before, Composer is *the* package manager for PHP-based projects. +> You can think of it as what NPM is to JavaScript, *but better*. +> If you haven't used it before, you have to install a recent PHP version and Composer before you can proceed. +> On Ubuntu- or Debian-based systems, this would be as simple as this: +> +> ```bash +> $ sudo apt install php-cli php-mbstring php-xml composer +> ``` + +In your project directory, simply run the following command: + +```bash +$ composer require clue/frugal:dev-main +``` + +This isn't NPM, so this should only take a moment or two. + +## Running + +The next step after installing all dependencies is now serve this web application. +One of the nice properties of this project is that is *runs anywhere* (provided you have PHP installed of course). + +For example, you can run the above example using PHP's built-in webserver for +testing purposes like this: + +```bash +$ php -S 0.0.0.0:8080 app.php +``` + +You can now use your favorite webbrowser or command line tool to check your web +application responds as expected: + +```bash +$ curl -v http://localhost:8080/ +HTTP/1.1 200 OK +… + +Hello wörld! +``` + +And that's it already, you can now stop your timer. +If you've made it this far, you should have an understanding why X is so exciting. +As a next step, we would recommend checking out the [best practices](../../best-practices/) in order to deploy this to production. + +Happy hacking! diff --git a/docs/integrations/authentication.md b/docs/integrations/authentication.md new file mode 100644 index 0000000..1fac8f2 --- /dev/null +++ b/docs/integrations/authentication.md @@ -0,0 +1,13 @@ +# Authentication + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* HTTP Basic auth easy to implement +* Implementation as HTTP [middleware](../api/middleware.md) recommended +* JWT and OAuth possible +* Handling credentials application-specific, may take advantage of [database](database.md) +* See also [sessions](sessions.md) diff --git a/docs/integrations/database.md b/docs/integrations/database.md new file mode 100644 index 0000000..a0df390 --- /dev/null +++ b/docs/integrations/database.md @@ -0,0 +1,17 @@ +# Database + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Async APIs with [Promises](../async/promises.md) +* Avoid using blocking PDO, Doctrine and family +* Major database vendors supported already + * [MySQL](https://github.com/friends-of-reactphp/mysql) + * [Postgres](https://github.com/voryx/PgAsync) + * [SQLite](https://github.com/clue/reactphp-sqlite) + * [Redis](https://github.com/clue/reactphp-redis) + * [ClickHouse](https://github.com/clue/reactphp-clickhouse) +* Future DBAL and ORM diff --git a/docs/integrations/filesystem.md b/docs/integrations/filesystem.md new file mode 100644 index 0000000..3d949df --- /dev/null +++ b/docs/integrations/filesystem.md @@ -0,0 +1,14 @@ +# Filesystem + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Async APIs with [Promises](../async/promises.md) +* Avoid using blocking `fopen()`, `file_get_contents()` and family +* Few blocking calls *can* be acceptable +* See [reactphp/filesystem](https://github.com/reactphp/filesystem) for filesystem prototypepq +* Avoid blocking filesystem by using [child process](child-processes.md) +* See [clue/reactphp-s3](https://github.com/clue/reactphp-s3) for async S3 filesystem API (supporting Amazon S3, Ceph, MiniIO, DigitalOcean Spaces and others) diff --git a/docs/integrations/queueing.md b/docs/integrations/queueing.md new file mode 100644 index 0000000..b4e3437 --- /dev/null +++ b/docs/integrations/queueing.md @@ -0,0 +1,14 @@ +# Queueing + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Common requirement to offload work from frontend to background workers +* Major queue vendors supported already + * [BunnyPHP](https://github.com/jakubkulhan/bunny) for AMQP (RabbitMQ) + * [Redis](https://github.com/clue/reactphp-redis) with blocking lists and streams + * Experimental [STOMP](https://github.com/friends-of-reactphp/stomp) support for RabbitMQ, Apollo, ActiveMQ, etc. +* Future optionally built-in queueing support with no external dependencies, but swappable diff --git a/docs/integrations/sessions.md b/docs/integrations/sessions.md new file mode 100644 index 0000000..6ebd446 --- /dev/null +++ b/docs/integrations/sessions.md @@ -0,0 +1,12 @@ +# Sessions + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Session handling common requirement and not hard to implement +* Implementation as HTTP [middleware](../api/middleware.md) recommended +* Handling credentials application-specific, may take advantage of [database](database.md) +* See also [authentication](authentication.md) diff --git a/docs/integrations/templates.md b/docs/integrations/templates.md new file mode 100644 index 0000000..9bc16a2 --- /dev/null +++ b/docs/integrations/templates.md @@ -0,0 +1,14 @@ +# Templates + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Very common requirement, especially for [HTML pages](../api/response.md#html) +* Any template language possible + * [Twig](https://twig.symfony.com/) + * [Handlebars](https://github.com/salesforce/handlebars-php) + * [Mustache](https://github.com/bobthecow/mustache.php) +* Template files often loaded from [filesystem](filesystem.md) (avoid blocking) diff --git a/docs/more/architecture.md b/docs/more/architecture.md new file mode 100644 index 0000000..5fcf73f --- /dev/null +++ b/docs/more/architecture.md @@ -0,0 +1,13 @@ +# Architecture + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* HTTP request response semantics +* PHP runs everywhere +* shared nothing execution model (optional) +* ReactPHP (long-running optional) +* Async PHP diff --git a/docs/more/community.md b/docs/more/community.md new file mode 100644 index 0000000..0a32754 --- /dev/null +++ b/docs/more/community.md @@ -0,0 +1,20 @@ +# Community + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +Framework X will be released as open-source under the permissive MIT license. +This means it will be free as in free speech and as in free beer. + +We believe in open source and made a conscious decision to take this path. +Being open-source means we can foster a community to focus on building the best possible framework together. +Framework X builds on top of existing open-source projects and we want to give back to this community of awesome engineers and developers. +Being open to outside contributions means we can guarantee interoperability with a vivid ecosystem and ensure the longevity of the project. + +* Twitter [@x_framework](https://twitter.com/x_framework) +* GitHub discussions +* Support chat +* GitHub sponsors diff --git a/docs/more/philosophy.md b/docs/more/philosophy.md new file mode 100644 index 0000000..62ce5cd --- /dev/null +++ b/docs/more/philosophy.md @@ -0,0 +1,16 @@ +# Our philosophy + +> ⚠️ **Documentation still under construction** +> +> You're seeing an early draft of the documentation that is still in the works. +> Give feedback to help us prioritize. +> We also welcome [contributors](../more/community.md) to help out! + +* Motto: make easy things easy & hard things possible +* From quick prototyping (RAD) to production environment in hours +* Batteries included, but swappable +* Reuse where applicable, but accept some duplication +* Long-term support (LTS) and careful upgrade paths +* Promote best practices, but don't enfore certain style +* Runs anywhere +* Open and inclusive [community](community.md) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..4dbb56f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,46 @@ +site_name: Documentation +site_dir: build/docs +extra: + homepage: ../ + +theme: + name: material + features: + - navigation.sections + +markdown_extensions: + - pymdownx.highlight + - pymdownx.superfences + - toc: + permalink: true + +nav: + - Getting Started: +# - "Why?": "" + - getting-started/quickstart.md + - Best Practices: + - "Controller classes": best-practices/controllers.md + - best-practices/testing.md + - best-practices/deployment.md + - API: + - api/app.md + - api/middleware.md + - api/request.md + - api/response.md + - Async: + - async/promises.md + - async/coroutines.md + - async/fibers.md + - async/streaming.md + - "Child processes": async/child-processes.md + - Integrations: + - integrations/database.md + - integrations/filesystem.md + - integrations/authentication.md + - integrations/sessions.md + - integrations/templates.md + - integrations/queueing.md + - More: + - more/philosophy.md + - more/architecture.md + - more/community.md