Licensed under MIT License.
- Overview
- Routing
- Middleware
- Modules
- The Request
- The Response
- Error handling
- Testing
- Working with containers
- Igni's server
- Webserver configuration
Igni framework allows you to write extensible and middleware based REST applications which are PSR-7 and PSR-15 compliant.
Igni aims to be:
- Lightweight: Igni's source code is around 60KB.
- Extensible: Igni offers extension system that allows to use
PSR-15
middleware (withzend-stratigility
) and modular architecture. - Testable: Igni extends
zend-diactoros
(PSR-7
implementation) and allows to manually dispatch routes to perform end-to-end tests. - Easy to use: Igni exposes an intuitive and concise API. All you need to do to use it is to include composer autoloader.
composer install igniphp/framework
In a nutshell, you can define controllers and map them to routes in one step. When the route matches, the function is executed and the response is dispatched to the client.
Igni can be used with already configured web-server or with shipped server (if swoole
extension is installed).
Following example shows how you can use Igni without build in webserver:
<?php
require_once __DIR__.'/vendor/autoload.php';
// Setup application
$application = new Igni\Http\Application();
// Define routing
$application->get('/hello/{name}', function (\Psr\Http\Message\ServerRequestInterface $request) : \Psr\Http\Message\ResponseInterface {
return \Igni\Http\Response::fromText("Hello {$request->getAttribute('name')}");
});
// Run the application
$application->run();
First we include composer's autoloader.
Instantiation of application happens next than simply controller is attached to the GET /hello/{name}
request pattern.
And application is run.
Similar approach is taken when build-in server comes in place:
<?php
require_once __DIR__.'/vendor/autoload.php';
// Setup application and routes
$application = new Igni\Http\Application();
$application->get('/hello/{name}', function (\Psr\Http\Message\ServerRequestInterface $request) : \Psr\Http\Message\ResponseInterface {
return \Igni\Http\Response::fromText("Hello {$request->getAttribute('name')}");
});
// Run with the server
$application->run(new Igni\Http\Server());
Server instance is created and passed to application's run
method.
While using default settings it listens on incoming localhost connections at port 8080
.
The route is a pattern representation of expected URI requested by clients.
Route pattern can use a syntax where {var}
specifies a placeholder with name var
and
it matches a regex [^/]+.
.
<?php
$application->get('/users/{id}', function() {...});
$application->get('/books/{isbn}/page/{page}', function() {...});
You can specify a custom pattern after the parameter name, the pattern should be enclosed between <
and >
.
Here are some examples:
<?php
// Matches following get requests: /users/42, /users/1, but not /users/me
$application->get('/users/{id<\d+>}', function() {...});
// Matches following get requests: /users/42, /users/me
$application->get('/users/{name<\w+>}', function() {...});
Default value can be specified after parameter name and should follow ?
sign, example:
<?php
// Matches following get requests with default id(2): /users/42, /users
$application->get('/users/{id<\d+>?2}', function() {...});
// Matches following get requests with default id(me): /users/42, /users/me
$application->get('/users/{name<\w+>?me}', function() {...});
In order to make parameter optional just add ?
add the end of the name
<?php
// Matches following get requests: /users/42, /users
$application->get('/users/{id?}', function() {...});
Full example:
<?php
// Include composer's autoloader.
require_once __DIR__.'/vendor/autoload.php';
$application = new Igni\Http\Application();
$application->get('/hello/{name}', function ($request) {
return Igni\Http\Response::fromText("Hello: {$request->getAttribute('name')}");
});
$application->run();
Makes $controller
to listen on $route
pattern on http GET
request.
Should be used to read or retrieve resource. On success response should return 200
(OK) status code.
In case of error most often 404
(Not found) or 400
(Bad request) should be returned in the status code.
Makes $controller
to listen on $route
pattern on http POST
request.
Should be used to create new resources (can be also used as a wild card verb for operations that don't fit elsewhere).
On successful creation, response should return 201
(created) or 202
(accepted) status code.
In case of error most often 406
(not acceptable), 409
(conflict) 413
(request entity too large)
Makes $controller
to listen on $route
pattern on http PUT
request.
Should be used to update a specific resource (by an identifier) or a collection of resources.
Can also be used to create a specific resource if the resource identifier is known before-hand.
Response code scenario is same as post
method with additional 404
if resource to update was not found.
Makes $controller
to listen on $route
pattern on http PATCH
request.
As patch
should be used for modify resource. The difference is that patch
request
can contain only the changes to the resource, not the complete resource as put
or post
.
Makes $controller
to listen on $route
pattern on http DELETE
request.
Should be used to delete resource.
Makes $controller
to listen on $route
pattern on http OPTIONS
request.
Makes $controller
to listen on $route
pattern on http HEAD
request.
Middleware is an individual component participating in processing incoming request and the creation of resulting response.
Middleware can be used to:
- Handle authentication details
- Perform content negotiation
- Error handling
In Igni middleware can be any closure that accepts \Psr\Http\Message\ServerRequestInterface
and \Psr\Http\Server\RequestHandlerInterface
as parameters
and returns valid instance of \Psr\Http\Message\ResponseInterface
or any class/object that implements \Psr\Http\Server\MiddlewareInterface
interface.
You can add as many middleware as you want, and they are triggered in the same order as you add them.
In fact even Igni\Http\Application
is a middleware itself which is automatically added at the end of the pipe.
<?php
// Include composer's autoloader.
require_once __DIR__.'/vendor/autoload.php';
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
class BenchmarkMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
$time = microtime(true);
$response = $next->handle($request);
$renderTime = microtime(true) - $time;
return $response->withHeader('render-time', $renderTime);
}
}
$application = new Igni\Http\Application();
// Attach custom middleware instance.
$application->use(new BenchmarkMiddleware());
// Attach closure middleware.
$application->use(function(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface {
$response = $next->handle($request);
return $response->withHeader('foo', 'bar');
});
// Run the application.
$application->run();
Module is a reusable part of application or business logic. It can listen on application state or/and extend application by providing additional middleware, services, models, libraries etc...
In Igni module is any class implementing any listener or provider interface.
The following list contains all possible interfaces that module can implement in order to provide additional features for the application:
- Listeners:
Igni\Application\Listeners\OnBootListener
Igni\Application\Listeners\OnErrorListener
Igni\Application\Listeners\OnRunListener
Igni\Application\Listeners\OnShutdownListener
- Providers:
Igni\Application\Providers\ConfigProvider
Igni\Application\Providers\ControllerProvider
Igni\Application\Providers\ServiceProvider
Example:
<?php
require_once __DIR__.'/vendor/autoload.php';
use Igni\Application\Controller\ControllerAggregate;
use Igni\Application\Providers\ControllerProvider;
use Igni\Http\Application;
use Igni\Http\Response;
use Igni\Http\Route;
/**
* Module definition.
*/
class SimpleModule implements ControllerProvider
{
public function provideControllers(ControllerAggregate $controllers): void
{
// Add controller that greets client when /hello/{name} URI is requested
$controllers->add(function ($request) {
return Response::fromText("Hello {$request->getAttribute('name')}!");
}, Route::get('/hello/{name}'));
}
}
$application = new Application();
// Extend application with the module.
$application->extend(\SimpleModule::class);
// Run the application.
$application->run();
OnBootListener can be implemented to perform tasks on application in boot state.
OnRunListener can be implemented to perform tasks which are dependent on various services provided by extensions.
Can be used for cleaning-up tasks.
Config provider is used to provide additional configuration settings to config service \Igni\Application\Config
.
Controller provider is used to register controllers within the application.
Makes usage of PSR compatible DI of your choice (If none is passed to application igniphp/container implementation will be used as default) to register additional services.
Igni uses invokable controllers (single action controllers). There few reasons for that:
- Controller can effectively wrap simple functionality into well defined namespace
- Less dependencies are required to perform the action
- Can be easy replaced with functions
- Does not break SRP
- Makes testing easier
In order to define controller one must implement Igni\Http\Controller
interface.
The following code contains an example controller which contains welcome message in the response.
<?php
// Include composer's autoloader.
require_once __DIR__.'/vendor/autoload.php';
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Igni\Http\Route;
class WelcomeUserController implements Igni\Http\Controller
{
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
return \Igni\Http\Response::fromText("Hi {$request->getAttribute('name')}!");
}
public static function getRoute(): Route
{
return Route::get('hi/{name}');
}
}
Controller can be registered either in your module file
or simply by calling add
method on application's controller aggregate:
$application->getControllerAggregate()->add(WelcomeUserController::class);
Igni's controllers and middleware are given a PSR-7 server request object that represents http request send by the client. Request contains route's params, body, request method, request uri and so on.
For information how to work with PSR-7 read this.
Igni's controllers and middleware must return valid PSR-7 response object.
Igni's Igni\Http\Response
class provides factories methods to simplify response creation.
Creates empty PSR-7 response object.
Creates PSR-7 request with content type set to text/plain
and body containing passed $text
Creates PSR-7 request with content type set to application/json
and body containing json data.
$data
can be array or \JsonSerializable
instance.
Creates PSR-7 request with content type set to text/html
and body containing passed html.
Creates PSR-7 request with content type set to application/xml
and body containing xml string.
$data
can be \SimpleXMLElement
, \DOMDocument
or just plain string.
Igni provides default error handler so if anything goes wrong in your application the error will not be
directly propagated to the client layer.
\Igni\Http\Middleware\ErrorMiddleware
is responsible for the error handling.
In any case you would like to provide custom error handler, it can be done by simply creating middleware with try/catch
statement inside process
method.
The following code returns custom response in case any error occurs in the application:
<?php
// Include composer's autoloader.
require_once __DIR__.'/vendor/autoload.php';
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
class CustomErrorHandler implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $next): ResponseInterface
{
try {
$response = $next->handle($request);
} catch (Throwable $throwable) {
$response = \Igni\Http\Response::fromText('Custom error message', $status = 500);
}
return $response;
}
}
$application = new Igni\Http\Application();
$application->use(new CustomErrorHandler());
// Run the application.
$application->run();
Igni is build to be testable and maintainable in fact most of the crucial framework's layers are covered with reasonable amount of tests.
Testing your code can be simply performed by executing your controller with mocked ServerRequest object, consider following example:
<?php declare(strict_types=1);
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Igni\Http\Route;
class WelcomeUserController implements Igni\Http\Controller
{
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
return \Igni\Http\Response::fromText("Hi {$request->getAttribute('name')}!");
}
public static function getRoute(): Route
{
return Route::get('hi/{name}');
}
}
final class WelcomeUserControllerTest extends TestCase
{
public function testWelcome(): void
{
$controller = new WelcomeUserController();
$response = $controller(\Igni\Http\ServerRequest::fromUri('/hi/Tom'));
self::assertSame('Hi Tom!', (string) $response->getBody());
self::assertSame(200, $response->getStatusCode());
}
}
By default Igni is using its own dependency injection container, which provides:
- easy to use interface
- autowiring support
- contextual injection
- free of any configuration or complex building process
- small footprint
So if you are fan of small and easy-to-use solutions there are no steps required in order to use it within your application.
Igni can work with any dependency injection container that is PSR-11 compatible service.
In order to use your favourite DI library just pass it as parameter to application's constructor.
If you container requires building process and you would like to use ServiceProvider
interface,
it is recommended to provide services as you would do this usually with your modules and attach OnRunListener
to any of your modules and build your container in the provided method:
<?php
// Include composer's autoloader.
require_once __DIR__.'/vendor/autoload.php';
use Igni\Application\Application;
use Igni\Application\Providers\ServiceProvider;
use Igni\Application\Listeners\OnRunListener;
use Psr\Container\ContainerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class SymfonyDependencyInjectionModule implements ServiceProvider, OnRunListener
{
public function onRun(Application $application)
{
/** @var ContainerBuilder $container */
$container = $application->getContainer();
$container->compile();
}
/**
* @param ContainerBuilder $container
*/
public function provideServices(ContainerInterface $container): void
{
$container->register('mailer', 'Mailer');
}
}
$containerBuilder = new ContainerBuilder();
$application = new Igni\Http\Application($containerBuilder);
$application->use(new SymfonyDependencyInjectionModule());
// Run the application.
$application->run();
Igni is shipped with an async non-blocking IO, multiple process HTTP server.
The server requires swoole
extension to be installed and enabled,
more information about swoole can be found here.
Linux users:
pecl install swoole
Mac users with homebrew:
brew install swoole
or:
brew install homebrew/php/php71-swoole
<?php
// Autoloader.
require_once __DIR__.'/vendor/autoload.php';
// Create server instance.
$server = new \Igni\Http\Server();
$server->start();
Igni http server uses event-driven model that makes it easy to scale and extend.
There are five type of events available, each of them extends Igni\Http\Server\Listener
interface:
Igni\Http\Server\OnStart
fired when server startsIgni\Http\Server\OnStop
fired when server stopsIgni\Http\Server\OnConnect
fired when new client connects to the serverIgni\Http\Server\OnClose
fired when connection with the client is closedIgni\Http\Server\OnRequest
fired when new request is dispatched
<?php
// Autoloader.
require_once __DIR__.'/vendor/autoload.php';
// Create server instance.
$server = new \Igni\Http\Server();
// Each request will retrieve 'Hello' response
$server->addListener(new class implements \Igni\Http\Server\OnRequest {
public function onRequest(\Psr\Http\Message\ServerRequestInterface $request): \Psr\Http\Message\ResponseInterface {
return \Igni\Http\Response::fromText('Hello');
}
});
$server->start();
Server can be easily configured with Igni\Http\Server\HttpConfiguration
class.
Please consider following example:
<?php
// Autoloader.
require_once __DIR__.'/vendor/autoload.php';
// Listen on localhost at port 80.
$configuration = new \Igni\Http\Server\HttpConfiguration('0.0.0.0', 80);
// Create server instance.
$server = new \Igni\Http\Server($configuration);
$server->start();
<?php
// Autoloader.
require_once __DIR__.'/vendor/autoload.php';
$configuration = new \Igni\Http\Server\HttpConfiguration();
$configuration->enableSsl($certFile, $keyFile);
// Create server instance.
$server = new \Igni\Http\Server($configuration);
$server->start();
<?php
// Autoloader.
require_once __DIR__.'/vendor/autoload.php';
$configuration = new \Igni\Http\Server\HttpConfiguration();
$configuration->enableDaemon($pidFile);
// Create server instance.
$server = new \Igni\Http\Server($configuration);
$server->start();
If you are using Apache, make sure mod_rewrite is enabled and use the following .htaccess file:
<IfModule mod_rewrite.c>
Options -MultiViews
RewriteEngine On
#RewriteBase /path/to/app
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]
</IfModule>
If you are using nginx + php-fpm, the following is minimal configuration to get the things done:
server {
server_name domain.tld www.domain.tld;
root /var/www/project/web;
location / {
# try to serve file directly, fallback to front controller
try_files $uri /index.php$is_args$args;
}
#
location ~ ^/index\.php(/|$) {
fastcgi_split_path_info ^(.+\.php)(/.*)$;
if (!-f $document_root$fastcgi_script_name) {
return 404;
}
# For socket connection
fastcgi_pass unix:/var/run/php-fpm.sock;
# Uncomment following line to use tcp connection instead socket
# fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
# Prevents URIs that include the front controller. This will 404:
# http://domain.tld/index.php/some-path
# Enable the internal directive to disable URIs like this
# internal;
}
#return 404 for all php files as we do have a front controller
location ~ \.php$ {
return 404;
}
error_log /var/log/nginx/project_error.log;
access_log /var/log/nginx/project_access.log;
}
PHP ships with a built-in webserver for development. This server allows you to run Igni without any configuration.
<?php
require_once __DIR__.'/vendor/autoload.php';
// Setup application and routes
$application = new Igni\Http\Application();
$application->get('/hello/{name}', function (\Psr\Http\Message\ServerRequestInterface $request) : \Psr\Http\Message\ResponseInterface {
return \Igni\Http\Response::fromText("Hello {$request->getAttribute('name')}");
});
// Run with the server
if (php_sapi_name() == 'cli-server') {
$application->run();
} else {
$application->run(new Igni\Http\Server());
}
Assuming your front controller is at ./index.php, you can start the server using the following command:
php -S localhost:8080 index.php
Note: This should be used only for development.