├── .gitignore ├── README.md ├── bootstrap.php ├── composer.json ├── src ├── Bootstrapper.php ├── BootstrapperInterface.php ├── Symfony │ └── Component │ │ ├── Console │ │ ├── ApplicationHandler.php │ │ ├── Input │ │ │ └── InputInterfaceSingleton.php │ │ └── Output │ │ │ └── OutputInterfaceSingleton.php │ │ └── HttpFoundation │ │ └── RequestSingleton.php └── SymfonyBootstrapper.php └── symfony-bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tchwork Bootstrapper 2 | ==================== 3 | 4 | What if we could decouple the bootstrapping logic of our apps from any global state? 5 | 6 | This package makes it possible with a few conventions. 7 | 8 | BootstrapperInterface 9 | --------------------- 10 | 11 | The core of this package is the `BootstrapperInterface`, which describes a high-order bootstrapping logic. 12 | 13 | It is designed to be totally generic and able to run any application outside of the global state in 6 steps: 14 | 15 | 1. your front-controller returns a `Closure` that wraps your app; 16 | 2. `BootstrapperInterface::getRuntime()` is given this closure and returns a closure too (potentially the same but 17 | it could also be decorated) and its arguments (typically PHP superglobals turned into your domain objects); 18 | 3. the returned closure, let's call it the "runtime", is called with the arguments computed at the previous step; 19 | 4. the result of the runtime closure, the runtime closure and its arguments are all passed to `BootstrapperInterface::getHandler()`, 20 | which should return another closure, the "handler", that will handle the result itself; 21 | 5. the handler closure is now called with the result of the runtime closure as argument; 22 | 6. the PHP engine is terminated with the integer status code returned by the handler closure. 23 | 24 | This process is extremely flexible as it allows implementations of `BootstrapperInterface` to hook into any critical steps. 25 | 26 | Bootstrapping files 27 | ------------------- 28 | 29 | The simplest way to use this package is to require the provided `bootstrap.php` file or an equivalent *instead of* the typical `vendor/autoload.php` file. 30 | 31 | This will use an instance of `Bootstrapper` (see below) by default, but you can provide another implementation by using the `$_SERVER['APP_BOOTSTRAPPER']` variable. 32 | When provided, `$_SERVER['APP_BOOTSTRAPPER']` should be set to a class name or an instance of `BootstrapperInterface` that will be used to run the app. 33 | 34 | By design, requiring the `bootstrap.php` file *after* the `vendor/autoload.php` one will *not* do anything. 35 | This allows requiring your front-controller several times without any side-effect. 36 | 37 | If you are in the context of a Symfony app, you can include the `symfony-bootstrap.php` file instead, 38 | which sets `$_SERVER['APP_BOOTSTRAPPER']` to `SymfonyBootstrapper`, adding common Symfony bootstrapping logic to the process: 39 | 40 | - `.env` files are always loaded if they are found in the root dir of your app; 41 | - PHP warnings and notices are turned into `ErrorException`; 42 | - the `APP_ENV` and the `APP_DEBUG` environement variables are used to configure the mode in which the app should run; 43 | - on the command line, `-e|--env` allows forcing a specific value for `APP_ENV` and `--no-debug` allows forcing `APP_DEBUG` to `0`. 44 | 45 | Example 46 | ------- 47 | 48 | Take a Symfony default skeleton and require `tchwork/bootstrapper`: 49 | ```sh 50 | symfony new test-app --version=dev-master # Symfony 5.1 works best for the example 51 | cd test-app/ 52 | composer require tchwork/bootstrapper:@dev 53 | ``` 54 | 55 | Replace the content of the `public/index.php` file by: 56 | ```php 57 | getRuntime($closure); 20 | $result = $closure(...$arguments); 21 | $closure = $_SERVER['APP_BOOTSTRAPPER']->getHandler($result, $closure, $arguments); 22 | 23 | exit($closure($result)); 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tchwork/bootstrapper", 3 | "description": "Takes care of booting your app from global state provided by the engine", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Nicolas Grekas", 9 | "email": "p@tchwork.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Tchwork\\Bootstrapper\\": "src/" 15 | } 16 | }, 17 | "require": { 18 | "php": ">=7.2.5" 19 | }, 20 | "suggest": { 21 | "symfony/dotenv": "To use the SymfonyBootstrapper", 22 | "symfony/error-handler": "To use the SymfonyBootstrapper" 23 | }, 24 | "extra": { 25 | "branch-alias": { 26 | "dev-master": "1.0-dev" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Bootstrapper.php: -------------------------------------------------------------------------------- 1 | getParameters() as $parameter) { 12 | $class = 'Tchwork\Bootstrapper\\'.$parameter->getType()->getName().'Singleton'; 13 | 14 | if ('Tchwork\Bootstrapper\arraySingleton' === $class) { 15 | $arguments[] = $_SERVER; 16 | continue; 17 | } 18 | 19 | if (class_exists($class)) { 20 | $arguments[] = $class::get(); 21 | continue; 22 | } 23 | 24 | $arguments[] = null; 25 | } 26 | 27 | return [$closure, $arguments]; 28 | } 29 | 30 | public function getHandler(object $result, \Closure $closure, array $arguments): callable 31 | { 32 | $class = \get_class($result); 33 | 34 | if (class_exists($c = 'Tchwork\Bootstrapper\\'.$class.'Handler')) { 35 | return [$c, 'handle']; 36 | } 37 | 38 | foreach (class_parents($class) as $c) { 39 | if (class_exists($c = 'Tchwork\Bootstrapper\\'.$c.'Handler')) { 40 | return [$c, 'handle']; 41 | } 42 | } 43 | 44 | foreach (class_implements($class) as $c) { 45 | if (class_exists($c = 'Tchwork\Bootstrapper\\'.$c.'Handler')) { 46 | return [$c, 'handle']; 47 | } 48 | } 49 | 50 | return static function (object $result): int { 51 | echo 'The runtime returned an unsupported value of type '.$class.".\n"; 52 | 53 | return 1; 54 | }; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/BootstrapperInterface.php: -------------------------------------------------------------------------------- 1 | run(Input\InputInterfaceSingleton::get(), Output\OutputInterfaceSingleton::get()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Symfony/Component/Console/Input/InputInterfaceSingleton.php: -------------------------------------------------------------------------------- 1 | getParameterOption(['--env', '-e'], null, true)) { 24 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 25 | } 26 | 27 | if ($input->hasParameterOption('--no-debug', true)) { 28 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 29 | } 30 | 31 | return self::$input = $input; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Symfony/Component/Console/Output/OutputInterfaceSingleton.php: -------------------------------------------------------------------------------- 1 | bootEnv($projectDir.'/.env'); 37 | 38 | if ($_SERVER['APP_DEBUG']) { 39 | umask(0000); 40 | Debug::enable(); 41 | } else { 42 | ErrorHandler::register(); 43 | } 44 | } 45 | 46 | public function getHandler(object $result, \Closure $closure, array $arguments): callable 47 | { 48 | if ($result instanceof HttpKernelInterface) { 49 | return static function ($kernel) { 50 | $request = RequestSingleton::get(); 51 | $response = $kernel->handle($request); 52 | $response->send(); 53 | 54 | if ($kernel instanceof TerminableInterface) { 55 | $kernel->terminate($request, $response); 56 | } 57 | 58 | return 0; 59 | }; 60 | } 61 | 62 | if ($result instanceof Response) { 63 | return static function ($response) { 64 | $response->send(); 65 | 66 | return 0; 67 | }; 68 | } 69 | 70 | return parent::getHandler($result, $closure, $arguments); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /symfony-bootstrap.php: -------------------------------------------------------------------------------- 1 |