Commit 83cbe49a authored by Mattia Bernasconi's avatar Mattia Bernasconi

Initial commit

parents
# FLT Swoole Engine
![Build Status](https://img.shields.io/travis/php-earth/swoole-engine/master.svg?style=flat-square)
Event-driven PHP engine for running PHP Applications with [Swoole extension](http://swoole.com).
## Installation
<details>
<summary>Before using this library, you'll need Swoole extension</summary>
Installing using PECL:
```bash
pecl install swoole
```
Add `extension=swoole` (or `extension=swoole.so` for PHP < 7.2) to your `php.ini`
file for PHP CLI sapi:
```bash
echo "extension=swoole" | sudo tee --append `php -r 'echo php_ini_loaded_file();'`
```
Check if Swoole extension is loaded
```bash
php --ri swoole
```
You should see something like
```bash
swoole
swoole support => enabled
Version => 2.0.10
Author => tianfeng.han[email: mikan.tenny@gmail.com]
epoll => enabled
eventfd => enabled
timerfd => enabled
signalfd => enabled
cpu affinity => enabled
spinlock => enabled
rwlock => enabled
async http/websocket client => enabled
Linux Native AIO => enabled
pcre => enabled
mutex_timedlock => enabled
pthread_barrier => enabled
futex => enabled
Directive => Local Value => Master Value
swoole.aio_thread_num => 2 => 2
swoole.display_errors => On => On
swoole.use_namespace => On => On
swoole.fast_serialize => Off => Off
swoole.unixsock_buffer_size => 8388608 => 8388608
```
</details>
Then proceed and install Swoole Engine library in your project with Composer:
```bash
composer require FLT/symfony-swoole
```
## Usage
Currently supported frameworks:
* Symfony:
```bash
vendor/bin/swoole [--env=dev|prod|...] [--host=IP] [--no-debug]
```
#!/usr/bin/env php
<?php
set_time_limit(0);
function includeIfExists($file) {
if (file_exists($file)) {
return require_once $file;
}
}
if ((!$loader = includeIfExists(__DIR__.'/../vendor/autoload.php')) && (!$loader = includeIfExists(__DIR__.'/../../../autoload.php'))) {
die(
'You must set up the project dependencies, run the following commands:'.PHP_EOL.
'curl -s http://getcomposer.org/installer | php'.PHP_EOL.
'php composer.phar install'
);
}
use Symfony\Component\Console\Application;
use FLT\Swoole\Command\ServerCommand;
$application = new Application('FLT Swoole Engine', 'v1.0.0');
$serverCommand = new ServerCommand();
$application->add($serverCommand);
$application->setDefaultCommand($serverCommand->getName(), true);
$application->run();
{
"name": "FLT/symfony-swoole",
"description": "Event-driven engine for running Symfony PHP with Swoole extension.",
"type": "library",
"require": {
"symfony/console": "^4.0",
"php": ">=7.1",
"ext-swoole": "*"
},
"require-dev": {
"phpunit/phpunit": "^6.1"
},
"authors": [
{
"name": "Mattia Bernasconi",
"homepage": "https://www.mattiabernasconi.it"
}
],
"autoload": {
"psr-4": {"FLT\\Swoole\\": "src/"}
},
"autoload-dev": {
"psr-4": { "FLT\\Swoole\\Tests\\": "tests/" }
},
"bin": ["bin/swoole"]
}
<?php
namespace FLT\Swoole;
class Accessor
{
/**
* Changes private or protected value of property of given object.
*
* @param object $object Object for which property needs to be changed
* @param string $property Property name
* @param mixed $value New value of private or protected property
*/
public static function set($object, $property, $value)
{
$thief = \Closure::bind(function($obj) use ($property, $value) {
$obj->$property = $value;
}, null, $object);
$thief($object);
}
/**
* Get private or protected property of given object.
*
* @param object $object Object for which property needs to be accessed.
* @param string $property Property name
*/
public static function get($object, $property)
{
return (function() use ($property) { return $this->$property; })->bindTo($object, $object)();
}
/**
* Binds callable for calling private and protected methods.
*
* @param callable $callable
* @param mixed $newThis
* @param array $args
* @param mixed $bindClass
* @return void
*/
public static function call(callable $callable, $newThis, $args = [], $bindClass = null)
{
$closure = \Closure::bind($callable, $newThis, $bindClass ?: get_class($newThis));
if ($args) {
call_user_func_array($closure, $args);
} else {
// Calling it directly is faster
$closure();
}
}
}
<?php
namespace FLT\Swoole\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use FLT\Swoole\Driver\Symfony\Driver;
/**
* Run Swoole HTTP server.
*
* bin/swoole server:start [--host=HOST] [--port=PORT] [--env=ENV] [--no-debug]
*/
class ServerCommand extends Command
{
private $driver;
protected function configure()
{
$this
->setName('server:start')
->setDescription('Start Swoole HTTP Server.')
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Host for server', '127.0.0.1')
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Port for server', 9501)
->addOption('env', null, InputOption::VALUE_OPTIONAL, 'Environment', 'prod')
->addOption('no-debug', null, InputOption::VALUE_NONE, 'Switch debug mode on/off')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$http = new \swoole_http_server($input->getOption('host'), $input->getOption('port'));
$http->set(array(
'worker_num' => 8
));
$this->driver = new Driver();
$debug = ($input->getOption('no-debug')) ? false : (($input->getOption('env') == 'prod') ? false : true);
$this->driver->boot($input->getOption('env'), $debug);
$http->on('request', function(\swoole_http_request $request, \swoole_http_response $response) use($output) {
$this->driver->setSwooleRequest($request);
$this->driver->setSwooleResponse($response);
$this->driver->preHandle();
$this->driver->handle();
$this->driver->postHandle();
$output->writeln($this->driver->symfonyRequest->getPathInfo());
});
$output->writeln('Swoole HTTP Server started on '.$input->getOption('host').':'.$input->getOption('port'));
$http->start();
}
}
<?php
namespace FLT\Swoole\Driver\Symfony;
use App\Kernel;
use FLT\Swoole\Driver\Symfony\Request;
use FLT\Swoole\Accessor;
use FLT\Swoole\Driver\Symfony\SessionStorage;
use Symfony\Component\Debug\Debug;
use Symfony\Component\Dotenv\Dotenv;
/**
* Driver for running Symfony with Swoole.
*/
class Driver
{
public $kernel;
public $symfonyRequest;
public $symfonyResponse;
private $swooleRequest;
private $swooleResponse;
private $projectDir = __DIR__.'/../../../../../..';
/**
* Boot Symfony Application.
*
* @param string $env Application environment
* @param bool $debug Switches debug mode on/off
*/
public function boot($env = 'dev', $debug = true)
{
$loader = require $this->projectDir.'/vendor/autoload.php';
// The check is to ensure we don't use .env in production
if (!isset($_SERVER['APP_ENV'])) {
if (!class_exists(Dotenv::class)) {
throw new \RuntimeException('APP_ENV environment variable is not defined. You need to define environment variables for configuration or add "symfony/dotenv" as a Composer dependency to load variables from a .env file.');
}
(new Dotenv())->load($this->projectDir.'/.env');
}
if ($_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev'))) {
umask(0000);
Debug::enable();
}
$this->kernel = new Kernel($_SERVER['APP_ENV'] ?? 'dev', $_SERVER['APP_DEBUG'] ?? ('prod' !== ($_SERVER['APP_ENV'] ?? 'dev')));
$this->kernel->boot();
}
/**
* Set Swoole request.
*
* @param \swoole_http_request $request
*/
public function setSwooleRequest(\swoole_http_request $request)
{
$this->swooleRequest = $request;
}
/**
* Set Swoole response.
*
* @param \swoole_http_response $response
*/
public function setSwooleResponse(\swoole_http_response $response)
{
$this->swooleResponse = $response;
}
/**
* Happens before each request. We need to change session storage service in
* the middle of Kernel booting process.
*
* @return void
*/
public function preHandle()
{
// Reset Kernel startTime, so Symfony can correctly calculate the execution time
if (Accessor::get($this->kernel, 'debug')) {
Accessor::set($this->kernel, 'startTime', microtime(true));
}
/* DISABLE SESSION! */
//$this->reloadSession();
Accessor::call(function() {
$this->initializeBundles();
$this->initializeContainer();
}, $this->kernel);
/* DISABLE SESSION! */
/*
if ($this->kernel->getContainer()->has('session')) {
// Inject custom SessionStorage of Symfony Driver
$nativeStorage = new SessionStorage(
$this->kernel->getContainer()->getParameter('session.storage.options'),
$this->kernel->getContainer()->has('session.handler') ? $this->kernel->getContainer()->get('session.handler'): null,
$this->kernel->getContainer()->get('session.storage')->getMetadataBag()
);
$nativeStorage->swooleResponse = $this->swooleResponse;
$this->kernel->getContainer()->set('session.storage.native', $nativeStorage);
}
*/
Accessor::call(function() {
foreach ($this->getBundles() as $bundle) {
$bundle->setContainer($this->container);
$bundle->boot();
}
$this->booted = true;
}, $this->kernel);
}
/**
* Happens after each request.
*
* @return void
*/
public function postHandle()
{
// Close database connection.
if ($this->kernel->getContainer()->has('doctrine.orm.entity_manager')) {
$this->kernel->getContainer()->get('doctrine.orm.entity_manager')->clear();
$this->kernel->getContainer()->get('doctrine.orm.entity_manager')->close();
$this->kernel->getContainer()->get('doctrine.orm.entity_manager')->getConnection()->close();
}
$this->kernel->terminate($this->symfonyRequest, $this->symfonyResponse);
}
/**
* Transform Symfony request and response to Swoole compatible response.
*
* @return void
*/
public function handle()
{
$rq = new Request();
$this->symfonyRequest = $rq->createSymfonyRequest($this->swooleRequest);
$this->symfonyResponse = $this->kernel->handle($this->symfonyRequest);
// Manually create PHP session cookie. When running Swoole, PHP session_start()
// function cannot set PHP session cookie since there is no traditional
// header outputting.
/* DISABLE SESSION! */
/*
if (!isset($this->swooleRequest->cookie[session_name()]) &&
$this->symfonyRequest->hasSession()
) {
$params = session_get_cookie_params();
$this->swooleResponse->rawcookie(
$this->symfonyRequest->getSession()->getName(),
$this->symfonyRequest->getSession()->getId(),
$params['lifetime'] ? time() + $params['lifetime'] : null,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
*/
// HTTP status code for response
$this->swooleResponse->status($this->symfonyResponse->getStatusCode());
// Cookies
foreach ($this->symfonyResponse->headers->getCookies() as $cookie) {
$this->swooleResponse->rawcookie(
$cookie->getName(),
urlencode($cookie->getValue()),
$cookie->getExpiresTime(),
$cookie->getPath(),
$cookie->getDomain(),
$cookie->isSecure(),
$cookie->isHttpOnly()
);
}
// Headers
foreach ($this->symfonyResponse->headers->allPreserveCase() as $name => $values) {
//$name = implode('-', array_map('ucfirst', explode('-', $name)));
foreach ($values as $value) {
$this->swooleResponse->header($name, $value);
}
}
$this->swooleResponse->end($this->symfonyResponse->getContent());
}
/**
* Fix for managing sessions with Swoole. On each request session_id needs to be
* regenerated, because we're running PHP script in CLI and listening for requests
* concurrently.
*
* @return void
*/
private function reloadSession()
{
if (isset($this->swooleRequest->cookie[session_name()])) {
session_id($this->swooleRequest->cookie[session_name()]);
} else {
if (session_id()) {
session_id(\bin2hex(\random_bytes(32)));
}
// Empty global session array otherwise it is filled with values from
// previous session.
$_SESSION = [];
}
}
}
<?php
namespace FLT\Swoole\Driver\Symfony;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class Request
{
/**
* Creates Symfony request from Swoole request. PHP superglobals must get set
* here.
*
* @param \swoole_http_request $request
* @return Request
*/
public function createSymfonyRequest(\swoole_http_request $request)
{
$this->setServer($request);
// Other superglobals
$_GET = $request->get ?? [];
$_POST = $request->post ?? [];
$_COOKIE = $request->cookie ?? [];
$_FILES = $request->files ?? [];
$content = $request->rawContent() ?: null;
$symfonyRequest = new SymfonyRequest(
$_GET,
$_POST,
[],
$_COOKIE,
$_FILES,
$_SERVER,
$content
);
if (0 === strpos($symfonyRequest->headers->get('Content-Type'), 'application/json')) {
$data = json_decode($request->rawContent(), true);
$symfonyRequest->request->replace(is_array($data) ? $data : []);
}
if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) {
$symfonyRequest::setTrustedProxies(explode(',', $trustedProxies), SymfonyRequest::HEADER_X_FORWARDED_ALL ^ SymfonyRequest::HEADER_X_FORWARDED_HOST);
}
if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) {
$symfonyRequest::setTrustedHosts(explode(',', $trustedHosts));
}
return $symfonyRequest;
}
/**
* Create $_SERVER superglobal for traditional PHP applications. By default
* Swoole request contains headers with lower case keys and dash separator
* instead of underscores and upper case letters which PHP expects in the
* $_SERVER superglobal. For example:
* - host: localhost:9501
* - connection: keep-alive
* - accept-language: en-US,en;q=0.8,sl;q=0.6
*
* @param \swoole_http_request $request
*/
public function setServer($request)
{
$headers = [];
foreach ($request->header as $key => $value) {
if ($key == 'x-forwarded-proto' && $value == 'https') {
$request->server['HTTPS'] = 'on';
}
$headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $key));
$headers[$headerKey] = $value;
}
// Make swoole's server's keys uppercased and merge them into the $_SERVER superglobal
$_SERVER = array_change_key_case(array_merge($request->server, $headers), CASE_UPPER);
}
}
<?php
namespace FLT\Swoole\Driver\Symfony;
/**
* Inspired by PHP-PM. Session id is not regenerated for each request since session_destroy()
* doesn't reset the session_id() nor does it regenerate a new one for a new session,
* here the default Symfony NativeSessionStorage uses PHP session_regenerated_id()
* with weaker ids. Here a better session id is generated.
*/
class SessionStorage extends \Symfony\Component\HttpFoundation\Session\Storage\NativeSessionStorage
{
public $swooleResponse;
/**
* {@inheritdoc}
*/
public function regenerate($destroy = false, $lifetime = null)
{
// NativeSessionStorage uses session_regenerate_id which also places a
// setcookie call, so we need to deactivate this, to not have
// additional Set-Cookie header set.
ini_set('session.use_cookies', 0);
if ($isRegenerated = parent::regenerate($destroy, $lifetime)) {
$params = session_get_cookie_params();
session_id(\bin2hex(\random_bytes(32)));
$this->swooleResponse->rawcookie(
session_name(),
session_id(),
$params['lifetime'] ? time() + $params['lifetime'] : null,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
ini_set('session.use_cookies', 1);
return $isRegenerated;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment