headio / phalcon-bootstrap
A flexible application bootstrap for phalcon projects.
Requires
- php: >=8.0
- ext-mbstring: *
- ext-phalcon: >= 5.0.0
Requires (Dev)
- codeception/codeception: ^4.1.0
- codeception/module-asserts: ^1.1
- codeception/module-phalcon5: ^1.0.1
- codeception/specify: ^1.4
- codeception/verify: ^1.5
- friendsofphp/php-cs-fixer: *
- mockery/mockery: ^1.3
- phalcon/ide-stubs: ^5.0.0
- symfony/console: ^5.0.0
- vimeo/psalm: ^4.11
README
A flexible application bootstrap for Phalcon-based projects
Description
This library provides flexible application bootstrapping, encapsulating module registration (or handler registration for micro applications), event management and middleware logic assignment for mvc, micro and cli-based applications. A simple factory instantiates the dependency injection container, encapsulating the registration of service dependency definitions defined in the configuration setttings.
Dependencies
- PHP >=8.0
- Phalcon >=5.0.0
See composer.json for more details
Installation
Composer
Open a terminal window and run:
composer require headio/phalcon-bootstrap
Usage
Micro applications (Api, prototype or micro service)
First create a config definition file inside your Phalcon project. This file should include the configuration settings, service & middleware definitions and a path to your handlers.
To get started, let's assume the following project structure:
├── public │ ├── index.php ├── src │ ├── Config │ │ │── Config.php │ │ │── Handlers.php │ │── Controller │ │── Domain │ │── Middleware │ │── Service │ │── Var │ │ │── Log ├── tests ├── vendor ├── Boot.php ├── codeception.yml ├── composer.json ├── .gitignore ├── README.md └── .travis.yml
and your PSR-4 autoload declaration is:
{ "autoload": { "psr-4": { "Foo\\": "src/" } } }
Create a config file Config.php inside the Config directory and copy-&-paste the following definition:
<?php namespace Foo\Config; use Foo\Middleware\NotFoundMiddleware; return [ 'applicationPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR, 'debug' => true, 'locale' => 'en_GB', 'logPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Var' . DIRECTORY_SEPARATOR . 'Log' . DIRECTORY_SEPARATOR, 'handlerPath' => __DIR__ . DIRECTORY_SEPARATOR . 'Handlers.php', 'middleware' => [ NotFoundMiddleware::class => 'before' ], 'services' => [ 'Foo\Service\EventManager', 'Foo\Service\Logger', ], 'timezone' => 'Europe\London' ];
The handlerPath declaration must include your handlers; the best strategy is to utilize Phalcon collections. The contents of this file might look something like this:
<?php namespace Foo\Config; use Foo\Controller\Index; use Phalcon\Mvc\Micro\Collection; $handler = new Collection(); $handler->setHandler(Index::class, true); $handler->setPrefix('/'); $handler->get('/', 'indexAction', 'apiIndex'); $app->mount($handler);
Now, create an index file inside the public directory and copy-&-paste the following:
<?php declare(strict_types=1); chdir(dirname(__DIR__)); require 'Boot.php';
Finally, paste the following bootstrap code inside the Boot.php file:
<?php declare(strict_types=1); use Headio\Phalcon\Bootstrap\Bootstrap; use Headio\Phalcon\Bootstrap\Di\Factory as DiFactory; use Phalcon\Config\Config; require_once __DIR__ . '/vendor/autoload.php'; // Micro example (function () { $config = new Config( require __DIR__ . '/src/Config/Config.php' ); $di = (new DiFactory($config))->createDefaultMvc(); // Environment if (extension_loaded('mbstring')) { mb_internal_encoding('UTF-8'); mb_substitute_character('none'); } set_error_handler( function ($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // Unmasked error context return; } throw new \ErrorException($message, 0, $severity, $file, $line); } ); set_exception_handler( function (Throwable $e) use ($di) { $di->get('logger')->error($e->getMessage(), ['exception' => $e]); // Verbose exception handling for development if ($di->get('config')->debug) { } exit(1); } ); // Run the application return Bootstrap::handle($di)->run($_SERVER['REQUEST_URI'], Bootstrap::Micro); })();
Mvc applications
Create a config definition file inside your Phalcon project. This file should include your configuration settings and service & middleware definitions.
Let's assume the following mvc project structure:
├── public │ ├── index.php ├── src │ ├── Config │ │ │── Config.php │ │── Controller │ │── Domain │ │── Middleware │ │── Module │ │ │── Admin │ │ │ │── Controller │ │ │ │── Form │ │ │ │── Task │ │ │ │── View │ │ │ │── Module.php │ │── Service │ │── Var │ │ │── Log ├── tests ├── vendor ├── Boot.php ├── codeception.yml ├── composer.json ├── .gitignore ├── README.md └── .travis.yml
and your PSR-4 autoload declaration is:
{ "autoload": { "psr-4": { "Foo\\": "src/" } } }
Create a config file Config.php inside the Config directory and copy-&-paste the following definition:
<?php namespace Foo\Config; return [ 'annotations' => [ 'adapter' => 'Apcu', 'options' => [ 'lifetime' => 3600 * 24 * 30, 'prefix' => 'annotations', ], ], 'applicationPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR, 'baseUri' => '/', 'debug' => true, 'dispatcher' => [ 'defaultAction' => 'index', 'defaultController' => 'Admin', 'defaultControllerNamespace' => 'Foo\\Module\\Admin\\Controller', 'defaultModule' => 'admin' ], 'locale' => 'en_GB', 'logPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Var' . DIRECTORY_SEPARATOR . 'Log' . DIRECTORY_SEPARATOR, 'modules' => [ 'admin' => [ 'className' => 'Foo\\Module\\Admin\\Module', 'path' => dirname(__DIR__) . '/Module/Admin/Module.php' ], ], 'middleware' => [ 'Foo\\Middleware\\Bar' ], 'routes' => [ 'admin' => [ 'Foo\Module\Admin\Controller\Admin' => '/admin', ], ], 'services' => [ 'Foo\Service\EventManager', 'Foo\Service\Logger', 'Foo\Service\Annotation', 'Foo\Service\Router', 'Foo\Service\View' ], 'timezone' => 'Europe\London', 'useI18n' => true, 'view' => [ 'defaultPath' => dirname(__DIR__) . '/Module/Admin/View/', 'compiledPath' => dirname(__DIR__) . '/Cache/Volt/', 'compiledSeparator' => '_', ] ];
Now, create an index file inside the public directory and paste the following:
<?php declare(strict_types=1); chdir(dirname(__DIR__)); require 'Boot.php';
Finally, paste the following bootstrap code inside the Boot.php file:
<?php declare(strict_types=1); use Headio\Phalcon\Bootstrap\Bootstrap; use Headio\Phalcon\Bootstrap\Di\Factory as DiFactory; use Phalcon\Config\Config; require_once __DIR__ . '/vendor/autoload.php'; // Mvc example (function () { $config = new Config( require __DIR__ . '/src/Config/Config.php' ); $di = (new DiFactory($config))->createDefaultMvc(); // Environment if (extension_loaded('mbstring')) { mb_internal_encoding('UTF-8'); mb_substitute_character('none'); } set_error_handler( function ($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // Unmasked error context return; } throw new \ErrorException($message, 0, $severity, $file, $line); } ); set_exception_handler( function (Throwable $e) use ($di) { $di->get('logger')->error($e->getMessage(), ['exception' => $e]); // Verbose exception handling for development if ($di->get('config')->debug) { } exit(1); } ); // Run the application return Bootstrap::handle($di)->run($_SERVER['REQUEST_URI']); })();
Console application
Create a config definition file inside your Phalcon project. This file should include your configuration settings and service & middleware definitions.
Let's assume the following project structure:
├── src │ ├── Config │ │ │── Config.php │ │── Domain │ │── Middleware │ │── Service │ │── Task │ │── Var │ │ │── Log ├── tests ├── vendor ├── Cli.php ├── codeception.yml ├── composer.json ├── .gitignore ├── README.md └── .travis.yml
and your PSR-4 autoload declaration is:
{ "autoload": { "psr-4": { "Foo\\": "src/" } } }
Create a config file Config.php inside the Config directory and copy-&-paste the following definition:
<?php namespace Foo\Config; return [ 'applicationPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR, 'locale' => 'en_GB', 'logPath' => dirname(__DIR__) . DIRECTORY_SEPARATOR . 'Var' . DIRECTORY_SEPARATOR . 'Log' . DIRECTORY_SEPARATOR, 'debug' => true, 'dispatcher' => [ 'defaultTaskNamespace' => 'Foo\\Task', ], 'middleware' => [ ], 'services' => [ 'Foo\Service\EventManager', 'Foo\Service\Logger', 'Foo\Service\ConsoleOutput', ], 'timezone' => 'Europe\London' ];
Finally, paste the following bootstrap code inside the Cli.php file:
<?php declare(strict_types=1); use Headio\Phalcon\Bootstrap\Cli\Bootstrap; use Headio\Phalcon\Bootstrap\Di\Factory as DiFactory; use Phalcon\Config\Config; require_once __DIR__ . '/vendor/autoload.php'; // Cli example (function () { $config = new Config( require __DIR__ . '/src/Config/Config.php' ); $di = (new DiFactory($config))->createDefaultCli(); // Environment set_error_handler( function ($severity, $message, $file, $line) { if (!(error_reporting() & $severity)) { // Unmasked error context return; } throw new \ErrorException($message, 0, $severity, $file, $line); } ); set_exception_handler( function (Throwable $e) use ($di) { $di->get('logger')->error($e->getMessage(), ['exception' => $e]); $output = $di->get('consoleOutput'); $output->writeln('<error>' . $e->getMessage() . '</error>'); // Verbose exception handling for development if ($di->get('config')->debug) { $output->writeln(sprintf( '<error>Exception thrown in: %s at line %d.</error>', $e->getFile(), $e->getLine()) ); } exit(1); } ); // Run the application return Bootstrap::handle($di)->run($_SERVER); })();
DI container factory
From the examples above you will have noticed that we instantiated Phalcon's factory default mvc or cli container services.
$config = new Config( require __DIR__ . '/src/Config/Config.php' ); // Micro/Mvc $di = (new DiFactory($config))->createDefaultMvc(); // Cli $di = (new DiFactory($config))->createDefaultCli();
Naturally, you can override the factory default services by simply defining a service definition in your config file, like so:
<?php namespace Foo\Config return [ 'services' => [ 'Foo\Service\Router' ] ]
Then create the respective service provider and modify its behaviour:
<?php /** * This source file is subject to the MIT License. * * For the full copyright and license information, please view * the LICENSE file that was distributed with this package. */ declare(strict_types=1); namespace Foo\Service; use Foo\Exception\OutOfRangeException; use Phalcon\Config\Config; use Phalcon\Di\ServiceProviderInterface; use Phalcon\Di\DiInterface; use Phalcon\Cli\Router as CliService; use Phalcon\Mvc\Router as MvcRouter; use Phalcon\Mvc\Router\Annotations as MvcService; class Router implements ServiceProviderInterface { /** * {@inheritDoc} */ public function register(DiInterface $di) : void { $di->setShared( 'router', function () use ($di) { $config = $di->get('config'); if ($config->get('cli')) { $service = new CliService(); $service->setDefaultModule($config->dispatcher->defaultTaskModule); return $service; } if (!$config->has('modules')) { throw new OutOfRangeException('Undefined modules'); } if (!$config->has('routes')) { throw new OutOfRangeException('Undefined routes'); } $service = new MvcService(false); $service->removeExtraSlashes(true); $service->setDefaultNamespace($config->dispatcher->defaultControllerNamespace); $service->setDefaultModule($config->dispatcher->defaultModule); $service->setDefaultController($config->dispatcher->defaultController); $service->setDefaultAction($config->dispatcher->defaultAction); foreach ($config->get('modules')->toArray() ?? [] as $module => $settings) { if (!$config->routes->get($module, false)) { continue; } foreach ($config->get('routes')->{$module}->toArray() ?? [] as $key => $val) { $service->addModuleResource($module, $key, $val); } } return $service; } ); } }
For complete control over the registration of service dependencies, or more generally, the services available in the container, you have two options: firstly, you can use Phalcon's base DI container, which is an empty container; or you can create your own DI container by implementing Phalcon's Phalcon\Di\DiInterface. See the following for an example:
use Phalcon\Di; use Foo\Bar\MyDi; $config = new Config( require __DIR__ . '/src/Config/Config.php' ); // Empty DI container $di = (new DiFactory($config))->create(new Di); // Custom DI container $di = (new DiFactory($config))->create(new MyDi);
The DI factory create method expects an instance of Phalcon\Di\DiInterface.
Application factory
The bootstrap factory will automatically instantiate a Phalcon application and return the response. If you want to bootstrap the application yourself, you can use the application factory directly.
<?php /** * This source file is subject to the MIT License. * * For the full copyright and license information, please view * the LICENSE file that was distributed with this package. */ declare(strict_types=1); use Headio\Phalcon\Bootstrap\Application\Factory as AppFactory; use Headio\Phalcon\Bootstrap\Di\Factory as DiFactory; use Phalcon\Config\Config; chdir(dirname(__DIR__)); require_once 'vendor/autoload.php'; $config = new Config( require 'src/Config/Config.php' ); try { $di = (new DiFactory($config))->createDefaultMvc(); /** @var Phalcon\Mvc\Application */ $app = (new AppFactory($di))->createForMvc(); // Do some stuff /** @var Phalcon\Mvc\ResponseInterface|bool */ $response = $app->handle($_SERVER['REQUEST_URI']); if ($response instanceof \Phalcon\Mvc\ResponseInterface) { return $response->send(); } return $response; } catch(\Throwable $e) { echo $e->getMessage(); }
Testing
To see the tests, run:
php vendor/bin/codecept run -f --coverage --coverage-text
License
Phalcon bootstrap is open-source and licensed under MIT License.