纯PHP-FPM框架(CodeIgniter4)兼容swoole改造

分组件改造

数据库(mysqli)

在Config\Database中重写框架原始数据库连接实例化方法

// app/Config/Database.php
class Database extends Config
{
    // ...其他内容

    // 新增这个方法
    public static function connect($group = null, bool $getShared = true) : \CodeIgniter\Database\ConnectionInterface
    {
        if(empty($group)){
            $cfg=config('Database');
            $group=$cfg->defaultGroup;
            $config=$cfg->$group;
        }elseif(is_string($group)){
            $cfg=config('Database');
            if(!isset($cfg->$group)) throw new \InvalidArgumentException('不存在的数据库连接组!');
            $config=$cfg->$group;
        }elseif(!($group instanceof \CodeIgniter\Database\ConnectionInterface)){
            $config=$group;
            $group='custom-'.md5(json_encode($config));
        }else return $group;
        if(empty($config['DBDriver'])) throw new \InvalidArgumentException('数据库驱动不能为空!');
        // method_exists会触发autoloader,为节约性能不宜使用
        // 如果将来PHP支持不触发autoloader查找类方法,将下面的if改成method_exists可以更直观
        if(class_exists('\Swoole\ConnectionPool',FALSE)&&\Swoole\Coroutine::getCid()!==-1){
            $connectionName='\App\Swoole\Database\\'.$config['DBDriver'].'\Connection';
            // 大多情况下,连接池中的连接都是拿来就能用的,因此这里完全不需要考虑$getShared参数
            if(class_exists($connectionName)) return new $connectionName($group,$config);
        }
        if($getShared&&isset(static::$instances[$group])) return static::$instances[$group];
        if(class_exists('\App\Database\\'.$config['DBDriver'].'\Connection')) $config['DBDriver']='\App\Database\\'.$config['DBDriver'];
        static::ensureFactory();
        return static::$instances[$group]=static::$factory->load($config, $group);
    }

在app/Database目录下增加几个自定义的改动,可以在这些文件里面扩展框架的数据库功能

app/Database/Trait_Builder.php
<?php namespace App\Database;
trait Trait_Builder {

    public function selectIntoTempTable(string $tb=NULL)
    {
        $tempTable=$this->db->createTempTable($this->compileSelect(),$tb,$this->binds,false);
        $this->resetSelect();
        $this->binds=[];
        return $tempTable;
    }
}

app/Database/Trait_Connection.php
<?php namespace App\Database;
trait Trait_Connection {

    /*临时表*/
    public function createTempTable(string $sql,string $name=NULL,...$args) : string
    {
        if(empty($name)){
            $name='temp_';
            do{
                $name.=chr(rand(65,90));
            }while(in_array($name,$this->tempTables));
        }
        $this->query("CREATE TEMPORARY TABLE `{$name}` ".$sql,...$args);
        return $this->tempTables[]=$name;
    }
    private array $tempTables=[];
    public function __destruct()
    {
        if(empty($this->connID)) return;
        foreach($this->tempTables AS $tb) $this->query("DROP TEMPORARY TABLE IF EXISTS `{$tb}`");
    }
}

app/Database/MySQLi/Builder.php
<?php namespace App\Database\MySQLi;
class Builder extends \CodeIgniter\Database\MySQLi\Builder {
    use \App\Database\Trait_Builder;
}

app/Database/MySQLi/Connection.php
<?php namespace App\Database\MySQLi;
class Connection extends \CodeIgniter\Database\MySQLi\Connection {
    use \App\Database\Trait_Connection;
}

app/Database/MySQLi/Forge.php
<?php namespace App\Database\MySQLi;
class Forge extends \CodeIgniter\Database\MySQLi\Forge {}

app/Database/MySQLi/PreparedQuery.php
<?php namespace App\Database\MySQLi;
class PreparedQuery extends \CodeIgniter\Database\MySQLi\PreparedQuery {}

app/Database/MySQLi/Result.php
<?php namespace App\Database\MySQLi;
class Result extends \CodeIgniter\Database\MySQLi\Result {
}

app/Database/MySQLi/Utils.php
<?php namespace App\Database\MySQLi;
class Utils extends \CodeIgniter\Database\MySQLi\Utils {
    
    // 从CI3中拷贝过来的,用不着的话可以不加这段
    public function _backup(array $params = null)
	{
		if (count($params) === 0)
		{
			return FALSE;
		}

		// Extract the prefs for simplicity
		extract($params);

		// Build the output
		$output = '';

		// Do we need to include a statement to disable foreign key checks?
		if ($foreign_key_checks === FALSE)
		{
			$output .= 'SET foreign_key_checks = 0;'.$newline;
		}

		foreach ( (array) $tables as $table)
		{
			// Is the table in the "ignore" list?
			if (in_array($table, (array) $ignore, TRUE))
			{
				continue;
			}

			// Get the table schema
			$query = $this->db->query('SHOW CREATE TABLE '.$this->db->escapeIdentifiers($this->db->database.'.'.$table));

			// No result means the table name was invalid
			if ($query === FALSE)
			{
				continue;
			}

			// Write out the table schema
			$output .= '#'.$newline.'# TABLE STRUCTURE FOR: '.$table.$newline.'#'.$newline.$newline;

			if ($add_drop === TRUE)
			{
				$output .= 'DROP TABLE IF EXISTS '.$this->db->protectIdentifiers($table).';'.$newline.$newline;
			}

			$i = 0;
			$result = $query->getResultArray();
			foreach ($result[0] as $val)
			{
				if ($i++ % 2)
				{
					$output .= $val.';'.$newline.$newline;
				}
			}

			// If inserts are not needed we're done...
			if ($add_insert === FALSE)
			{
				continue;
			}

			// Grab all the data from the current table
			$query = $this->db->query('SELECT * FROM '.$this->db->protectIdentifiers($table));

			if ($query->numRows === 0)
			{
				continue;
			}

			// Fetch the field names and determine if the field is an
			// integer type. We use this info to decide whether to
			// surround the data with quotes or not

			$i = 0;
			$field_str = '';
			$is_int = array();
			while ($field = $query->resultID->fetch_field())
			{
				// Most versions of MySQL store timestamp as a string
				$is_int[$i] = in_array(strtolower($field->type),
							array('tinyint', 'smallint', 'mediumint', 'int', 'bigint'), //, 'timestamp'),
							TRUE);

				// Create a string of field names
				$field_str .= $this->db->escapeIdentifiers($field->name).', ';
				$i++;
			}

			// Trim off the end comma
			$field_str = preg_replace('/, $/' , '', $field_str);

			// Build the insert string
			foreach ($query->getResultArray() as $row)
			{
				$val_str = '';

				$i = 0;
				foreach ($row as $v)
				{
					// Is the value NULL?
					if ($v === NULL)
					{
						$val_str .= 'NULL';
					}
					else
					{
						// Escape the data if it's not an integer
						$val_str .= ($is_int[$i] === FALSE) ? $this->db->escape($v) : $v;
					}

					// Append a comma
					$val_str .= ', ';
					$i++;
				}

				// Remove the comma at the end of the string
				$val_str = preg_replace('/, $/' , '', $val_str);

				// Build the INSERT string
				$output .= 'INSERT INTO '.$this->db->protectIdentifiers($table).' ('.$field_str.') VALUES ('.$val_str.');'.$newline;
			}

			$output .= $newline.$newline;
		}

		// Do we need to include a statement to re-enable foreign key checks?
		if ($foreign_key_checks === FALSE)
		{
			$output .= 'SET foreign_key_checks = 1;'.$newline;
		}

		return $output;
	}
}

划重点!!!
在app目录下建一个Swoole目录,专门用来放适配swoole的内容,这里先介绍数据库相关的,后面还有其他内容也要放到这个目录下。
改造数据库的核心文件有三个:

/app/Swoole/Database/Trait_Connection.php
<?php namespace App\Swoole\Database;
trait Trait_Connection {
    // 分组存放连接池的容器,键名为数据库参数组或自定义参数的json
    public static $pools=[];
    // 经测试,在trait中修改静态属性不会影响trait本身的静态属性
    // 当然,更不可能影响其他引用此trait的类的静态属性
    public function __construct(private string $group,...$args)
    {
        parent::__construct(...$args);
        $this->queryClass=Query::class;
        if(!isset(self::$pools[$group])) self::$pools[$group]=$this->createPool();
    }
    abstract private function createPool() : \Swoole\ConnectionPool;
    
	public function __destruct()
	{
        parent::__destruct();
		if(!empty($this->connID)) self::$pools[$this->group]->put($this->connID);
	}
    // __clone在新对象中执行,因此不会影响原对象的后续处理
	public function __clone()
    {
		if(!empty($this->connID)) $this->connID=NULL;
    }
}

/app/Swoole/Database/Query.php
<?php namespace App\Swoole\Database;
class Query extends \CodeIgniter\Database\Query
{
    // 如果这里不克隆,就会出现如下情形:
    // 一次SELECT查询后,db实例中有了一个lastQuery属性,lastQuery属性中又有一个db属性,指向创建它的db实例
    // 结果在资源释放的时候陷入死循环:db要释放lastQuery属性,lastQuery又要释放db属性
    public function __construct(\CodeIgniter\Database\ConnectionInterface $db)
    {
        $this->db=clone $db;
    }
}

/app/Swoole/Database/MySQLi/Connection.php
<?php namespace App\Swoole\Database\MySQLi;
final class Connection extends \App\Database\MySQLi\Connection {
    use \App\Swoole\Database\Trait_Connection;
    private function createPool() : \Swoole\ConnectionPool
    {
        $config=new \Swoole\Database\MysqliConfig();
        $config->withHost($this->hostname);
        $config->withPort($this->port);
        $config->withUsername($this->username);
        $config->withPassword($this->password);
        $config->withDbName($this->database);
        $config->withCharset($this->charset); //???未生效??
        // $config->withUnixSocket('/tmp/mysql.sock');
        return new \Swoole\Database\MysqliPool($config,DB_POOL_SIZE);
    }

    protected function _close()
    {
        $this->connID->close();
        self::$pools[$this->group]->put(NULL);
    }
    
    public function connect(bool $persistent = false)
    {
        if(!empty($this->connID)) $this->_close();
        $this->connID=self::$pools[$this->group]->get();
        return $this->mysqli=$this->connID;
    }
}
// mysql的默认空闲时长为8小时,超时后会自动断开连接,故每8小时要清空一次连接池
\Swoole\Timer::tick(28700000,function(){
    foreach(Connection::$pools AS $pool){
        for($i=DB_POOL_SIZE;$i!==0;$i--){
            $pool->get();
            $pool->put(NULL);
        }
    }
});

外加三个兼容性文件

/app/Swoole/Database/MySQLi/Builder.php
<?php namespace App\Swoole\Database\MySQLi;
class Builder extends \App\Database\MySQLi\Builder {
    //这里可以做一些协程化的处理,例如batch
}

/app/Swoole/Database/MySQLi/PreparedQuery.php
<?php namespace App\Swoole\Database\MySQLi;
class PreparedQuery extends \App\Database\MySQLi\PreparedQuery {}

/app/Swoole/Database/MySQLi/Result.php
<?php namespace App\Swoole\Database\MySQLi;
class Result extends \App\Database\MySQLi\Result {}

“全局”空间

曾经听过这么一种说法:越差劲的程序员越喜欢往全局空间塞变量。如果一个PHPer喜欢动不动就global一下,那TA大概是一个面向过程的爱好者,然而PHP早就转为面向对象语言了。
在FPM模式下大量使用全局变量通常只产生一些阅读方面的问题,但是在swoole中就万万不能这么干了,因为同一个worker进程中的所有request共用了同一个全局空间,并且在worker退出前会一直占着内存。可确实有一些变量需要在多处使用,并且要在请求结束后释放,同时又难以层层传递,咋办呢?
好在swoole提供了协程上下文这么个东西,我的思路就是每个请求都创建一个根级协程,在这个请求周期内用的全局变量都放到协程上下文中,请求结束后随着根协程一起自动释放。核心方法就几行,具体放哪见后面$app生命周期部分。

// 这个方法很简单,但是不推荐在业务代码中使用,而是提供一些专门的方法
function getRootContext()
{
    $pcid=Swoole\Coroutine::getPcid();
    while($pCtx=Swoole\Coroutine::getContext($pcid)){
        $ctx=$pCtx;
        $pcid=Swoole\Coroutine::getPcid($pcid);
    }
    return $ctx??Swoole\Coroutine::getContext();
}
// 例如存放共享类实例
function getSharedInstance(string $className)
{
    $ctx=getRootContext();
    return $ctx[$className]??$ctx[$className]=new $className();
}
// 以及覆盖框架的model和session方法
function model(string $name, bool $getShared = true, ?CodeIgniter\Database\ConnectionInterface &$conn = null)
{
    if(!$getShared) return CodeIgniter\Config\Factories::models($name, ['getShared' => FALSE], $conn);
    $rootContext=getRootContext();
    if(isset($rootContext[$name])) return $rootContext[$name];
    return $rootContext[$name]=CodeIgniter\Config\Factories::models($name, ['getShared' => FALSE], $conn);
}
function session(?string $val=null)
{
    global $sessionFactory;
    $ctx=getRootContext();
    if(!isset($ctx->session)) $ctx->session=$sessionFactory->create($ctx->request,$ctx->response,$ctx->logger);
    return is_string($val)?$ctx->session->get($val):$ctx->session;
}

HTTP请求和响应

如上所述,在swoole中已经很难再用全局变量了,那么在FPM中常用的$_GET、$_POST、$_SESSION等请求相关的变量自然也是不能用的。
另外,PHP进程中的echo是对进程启动者的响应,在FPM模式下,一个请求就会启动一个进程,可以理解为是用户启动了一个进程,因此很容易做到面向用户echo。而swoole进程通常是开发者启动的,就算不是开发者也是某个脚本启动,不会对应到用户,因此也就不可能面向用户echo,那么对用户的响应自然也就不能用echo了。因此swoole提供了HTTP\Response对象来解决这个问题。其实很多web服务的编程语言都是这么干的,比如nodejs中也有ServerResponse类,PHP中的echo可以类比为前端的console.log等,也即某些语言中的“标准输出(stdout)”,例如c#。

IncomingRequest类有一些没用到的内容,因为懒得花精力调试,干脆就没改,用到的都改完了:

<?php namespace App\Swoole\HTTP;
use CodeIgniter\HTTP\Exceptions\HTTPException;
use CodeIgniter\HTTP\Files\UploadedFile;
use CodeIgniter\HTTP\URI;
use Config\Services;
use Config\App;
use Locale;
class UserAgent extends \CodeIgniter\HTTP\UserAgent
{
    public function __construct(string $agent)
    {
        $this->agent=$agent;
        $this->config=new \Config\UserAgents();
        $this->compileData();
    }
}

class IncomingRequest extends \CodeIgniter\HTTP\Request
{
    /**
     * Enable CSRF flag
     *
     * Enables a CSRF cookie token to be set.
     * Set automatically based on Config setting.
     *
     * @var bool
     *
     * @deprecated Not used
     */
    protected $enableCSRF = false;

    /**
     * The URI for this request.
     *
     * Note: This WILL NOT match the actual URL in the browser since for
     * everything this cares about (and the router, etc) is the portion
     * AFTER the script name. So, if hosted in a sub-folder this will
     * appear different than actual URL. If you need that use getPath().
     *
     * @TODO should be protected. Use getUri() instead.
     *
     * @var URI
     */
    public $uri;

    /**
     * The detected path (relative to SCRIPT_NAME).
     *
     * Note: current_url() uses this to build its URI,
     * so this becomes the source for the "current URL"
     * when working with the share request instance.
     *
     * @var string|null
     */
    protected $path;

    /**
     * File collection
     *
     * @var FileCollection|null
     */
    protected $files;

    /**
     * Negotiator
     *
     * @var Negotiate|null
     */
    protected $negotiator;

    /**
     * The default Locale this request
     * should operate under.
     *
     * @var string
     */
    protected $defaultLocale;

    /**
     * The current locale of the application.
     * Default value is set in Config\App.php
     *
     * @var string
     */
    protected $locale;

    /**
     * Stores the valid locale codes.
     *
     * @var array
     */
    protected $validLocales = [];

    /**
     * Configuration settings.
     *
     * @var App
     */

    /**
     * Holds the old data from a redirect.
     *
     * @var array
     */
    protected $oldInput = [];

    /**
     * The user agent this request is from.
     *
     * @var UserAgent
     */
    protected $userAgent;

    public function __construct(public $config,private \Swoole\Http\Request $rqst)
    {
        $this->proxyIPs = $config->proxyIPs;
        $this->userAgent=new UserAgent(isset($rqst->header['user-agent'])?trim($rqst->header['user-agent']):'');
        $this->validLocales=$config->supportedLocales;
        $this->method=$rqst->server['request_method'];
        $this->body=$rqst->getContent()?:null;
        foreach($rqst->header AS $k => $v){
            $this->setHeader($k,$v);
            $this->headerMap[strtolower($k)] = $k;
        }
        $this->populateGlobals('server');
        $this->populateGlobals('get');

        $uri=URI::removeDotSegments($rqst->server['request_uri']);
        $this->path=($uri==='/'||$uri==='')?'/':ltrim($uri,'/');
        $this->uri=Services::uri($this->path,FALSE);
        $this->uri->setScheme(parse_url($config->baseURL, PHP_URL_SCHEME));
        $this->uri->setHost(parse_url($config->baseURL, PHP_URL_HOST));
        $this->uri->setPort(parse_url($config->baseURL, PHP_URL_PORT));
        $this->uri->setQuery($rqst->server['query_string']??'');
        if($config->forceGlobalSecureRequests&&$this->uri->getScheme()==='http') $this->uri->setScheme('https');
        
        $this->locale=$this->defaultLocale = $config->defaultLocale;
        if($config->negotiateLocale) $this->setLocale($this->negotiate('language',$config->supportedLocales));
    }


    public function getServer($index = null, $filter = null, $flags = null)
    {
        if($index){
            if (sscanf($index, 'HTTP_%s', $header) === 1) {
                // take SOME_HEADER and turn it into Some-Header
                $header = str_replace('_', ' ', strtolower($header));
                return $this->getHeaderLine(str_replace(' ', '-', ucwords($header)));
            }
            $index=strtolower($index);
        }
        return $this->fetchGlobal('server', $index, $filter, $flags);
    }

    protected function populateGlobals(string $method)
    {
        // Don't populate ENV as it might contain
        // sensitive data that we don't want to get logged.
        switch ($method) {
            case 'get':
                $this->globals['get'] = $this->rqst->get??[];
                break;

            case 'post':
                $this->globals['post'] = $this->rqst->post??[];
                break;

            case 'cookie':
                $this->globals['cookie'] = $this->rqst->cookie??[];
                break;

            case 'server':
                $this->globals['server'] = $this->rqst->server;
                break;
                
            default:
                $this->globals[$method] = [];
                break;
        }
    }


    /**
     * Provides a convenient way to work with the Negotiate class
     * for content negotiation.
     */
    public function negotiate(string $type, array $supported, bool $strictMatch = false): string
    {
        if ($this->negotiator === null) {
            $this->negotiator = Services::negotiator($this, true);
        }

        switch (strtolower($type)) {
            case 'media':
                return $this->negotiator->media($supported, $strictMatch);

            case 'charset':
                return $this->negotiator->charset($supported);

            case 'encoding':
                return $this->negotiator->encoding($supported);

            case 'language':
                return $this->negotiator->language($supported);
        }

        throw HTTPException::forInvalidNegotiationType($type);
    }

    /**
     * Determines if this request was made from the command line (CLI).
     */
    public function isCLI(): bool
    {
        return false;
    }

    /**
     * Test to see if a request contains the HTTP_X_REQUESTED_WITH header.
     */
    public function isAJAX(): bool
    {
        return $this->hasHeader('X-Requested-With') && strtolower($this->header('X-Requested-With')->getValue()) === 'xmlhttprequest';
    }

    /**
     * Attempts to detect if the current connection is secure through
     * a few different methods.
     */
    public function isSecure(): bool
    {
        if ($this->hasHeader('X-Forwarded-Proto') && $this->header('X-Forwarded-Proto')->getValue() === 'https') return true;
        return $this->hasHeader('Front-End-Https') && ! empty($this->header('Front-End-Https')->getValue()) && strtolower($this->header('Front-End-Https')->getValue()) !== 'off';
    }
    public function getPath(): string
    {
        return $this->path;
    }

    /**
     * Sets the locale string for this request.
     *
     * @return IncomingRequest
     */
    public function setLocale(string $locale)
    {
        // If it's not a valid locale, set it
        // to the default locale for the site.
        if (! in_array($locale, $this->validLocales, true)) {
            $locale = $this->defaultLocale;
        }

        $this->locale = $locale;
        Locale::setDefault($locale);

        return $this;
    }

    /**
     * Gets the current locale, with a fallback to the default
     * locale if none is set.
     */
    public function getLocale(): string
    {
        return $this->locale ?? $this->defaultLocale;
    }

    /**
     * Returns the default locale as set in Config\App.php
     */
    public function getDefaultLocale(): string
    {
        return $this->defaultLocale;
    }

    /**
     * Fetch an item from JSON input stream with fallback to $_REQUEST object. This is the simplest way
     * to grab data from the request object and can be used in lieu of the
     * other get* methods in most cases.
     *
     * @param array|string|null $index
     * @param int|null          $filter Filter constant
     * @param mixed             $flags
     *
     * @return mixed
     */
    public function getVar($index = null, $filter = null, $flags = null)
    {
        if (strpos($this->getHeaderLine('Content-Type'), 'application/json') !== false && $this->body !== null) {
            if ($index === null) {
                return $this->getJSON();
            }

            if (is_array($index)) {
                $output = [];

                foreach ($index as $key) {
                    $output[$key] = $this->getJsonVar($key, false, $filter, $flags);
                }

                return $output;
            }

            return $this->getJsonVar($index, false, $filter, $flags);
        }

        return $this->fetchGlobal('request', $index, $filter, $flags);
    }

    /**
     * A convenience method that grabs the raw input stream and decodes
     * the JSON into an array.
     *
     * If $assoc == true, then all objects in the response will be converted
     * to associative arrays.
     *
     * @param bool $assoc   Whether to return objects as associative arrays
     * @param int  $depth   How many levels deep to decode
     * @param int  $options Bitmask of options
     *
     * @see http://php.net/manual/en/function.json-decode.php
     *
     * @return mixed
     */
    public function getJSON(bool $assoc = false, int $depth = 512, int $options = 0)
    {
        return json_decode($this->body ?? '', $assoc, $depth, $options);
    }

    /**
     * Get a specific variable from a JSON input stream
     *
     * @param string         $index  The variable that you want which can use dot syntax for getting specific values.
     * @param bool           $assoc  If true, return the result as an associative array.
     * @param int|null       $filter Filter Constant
     * @param array|int|null $flags  Option
     *
     * @return mixed
     */
    public function getJsonVar(string $index, bool $assoc = false, ?int $filter = null, $flags = null)
    {
        helper('array');

        $json = $this->getJSON(true);
        if (! is_array($json)) {
            return null;
        }
        $data = dot_array_search($index, $json);

        if ($data === null) {
            return null;
        }

        if (! is_array($data)) {
            $filter ??= FILTER_DEFAULT;
            $flags = is_array($flags) ? $flags : (is_numeric($flags) ? (int) $flags : 0);

            return filter_var($data, $filter, $flags);
        }

        if (! $assoc) {
            return json_decode(json_encode($data));
        }

        return $data;
    }

    /**
     * A convenience method that grabs the raw input stream(send method in PUT, PATCH, DELETE) and decodes
     * the String into an array.
     *
     * @return mixed
     */
    public function getRawInput()
    {
        parse_str($this->body ?? '', $output);

        return $output;
    }

    /**
     * Fetch an item from GET data.
     *
     * @param array|string|null $index  Index for item to fetch from $_GET.
     * @param int|null          $filter A filter name to apply.
     * @param mixed|null        $flags
     *
     * @return mixed
     */
    public function getGet($index = null, $filter = null, $flags = null)
    {
        return $this->fetchGlobal('get', $index, $filter, $flags);
    }

    /**
     * Fetch an item from POST.
     *
     * @param array|string|null $index  Index for item to fetch from $_POST.
     * @param int|null          $filter A filter name to apply
     * @param mixed             $flags
     *
     * @return mixed
     */
    public function getPost($index = null, $filter = null, $flags = null)
    {
        return $this->fetchGlobal('post', $index, $filter, $flags);
    }

    public function getPostGet($index = null, $filter = null, $flags = null)
    {
        // Use $_POST directly here, since filter_has_var only
        // checks the initial POST data, not anything that might
        // have been added since.
        return isset($this->rqst->post[$index]) ? $this->getPost($index, $filter, $flags) : (isset($this->rqst->get[$index]) ? $this->getGet($index, $filter, $flags) : $this->getPost($index, $filter, $flags));
    }

    public function getGetPost($index = null, $filter = null, $flags = null)
    {
        // Use $_GET directly here, since filter_has_var only
        // checks the initial GET data, not anything that might
        // have been added since.
        return isset($this->rqst->get[$index]) ? $this->getGet($index, $filter, $flags) : (isset($this->rqst->post[$index]) ? $this->getPost($index, $filter, $flags) : $this->getGet($index, $filter, $flags));
    }

    /**
     * Fetch an item from the COOKIE array.
     *
     * @param array|string|null $index  Index for item to be fetched from $_COOKIE
     * @param int|null          $filter A filter name to be applied
     * @param mixed             $flags
     *
     * @return mixed
     */
    public function getCookie($index = null, $filter = null, $flags = null)
    {
        return $this->fetchGlobal('cookie', $index, $filter, $flags);
    }

    /**
     * Fetch the user agent string
     *
     * @return UserAgent
     */
    public function getUserAgent()
    {
        return $this->userAgent;
    }

    /**
     * Attempts to get old Input data that has been flashed to the session
     * with redirect_with_input(). It first checks for the data in the old
     * POST data, then the old GET data and finally check for dot arrays
     *
     * @return mixed
     */
    public function getOldInput(string $key)
    {
        // If the session hasn't been started, or no
        // data was previously saved, we're done.
        if (empty($_SESSION['_ci_old_input'])) {
            return null;
        }

        // Check for the value in the POST array first.
        if (isset($_SESSION['_ci_old_input']['post'][$key])) {
            return $_SESSION['_ci_old_input']['post'][$key];
        }

        // Next check in the GET array.
        if (isset($_SESSION['_ci_old_input']['get'][$key])) {
            return $_SESSION['_ci_old_input']['get'][$key];
        }

        helper('array');

        // Check for an array value in POST.
        if (isset($_SESSION['_ci_old_input']['post'])) {
            $value = dot_array_search($key, $_SESSION['_ci_old_input']['post']);
            if ($value !== null) {
                return $value;
            }
        }

        // Check for an array value in GET.
        if (isset($_SESSION['_ci_old_input']['get'])) {
            $value = dot_array_search($key, $_SESSION['_ci_old_input']['get']);
            if ($value !== null) {
                return $value;
            }
        }

        // requested session key not found
        return null;
    }

    /**
     * Returns an array of all files that have been uploaded with this
     * request. Each file is represented by an UploadedFile instance.
     */
    public function getFiles(): array
    {
        if ($this->files === null) {
            $this->files = new FileCollection($this->rqst->files);
        }

        return $this->files->all(); // return all files
    }

    /**
     * Verify if a file exist, by the name of the input field used to upload it, in the collection
     * of uploaded files and if is have been uploaded with multiple option.
     *
     * @return array|null
     */
    public function getFileMultiple(string $fileID)
    {
        if ($this->files === null) {
            $this->files = new FileCollection($this->rqst->files);
        }

        return $this->files->getFileMultiple($fileID);
    }

    /**
     * Retrieves a single file by the name of the input field used
     * to upload it.
     *
     * @return UploadedFile|null
     */
    public function getFile(string $fileID)
    {
        if ($this->files === null) {
            $this->files = new FileCollection($this->rqst->files);
        }

        return $this->files->getFile($fileID);
    }
}

这两个类改完了:

/app/Swoole/HTTP/FileCollection.php
<?php namespace App\Swoole\HTTP;
class FileCollection extends \CodeIgniter\HTTP\Files\FileCollection {

    public function __construct(array $files)
    {
        $this->files=[];
        if(!empty($files)) foreach($this->fixFilesArray($files) AS $name=>$file) $this->files[$name] = $this->createFileObject($file);
    }
}

/app/Swoole/HTTP/Response.php
<?php namespace App\Swoole\HTTP;
class Response extends \CodeIgniter\HTTP\Response{
    
    public function __construct($config,public \Swoole\Http\Response $resp,private \Swoole\Http\Request $rqst)
    {
        parent::__construct($config);
    }

    
    /**
     * Sends the headers of this HTTP response to the browser.
     *
     * @return Response
     */
    private bool $headersSent=FALSE;
    public function sendHeaders()
    {
        // Have the headers already been sent?
        if ($this->pretend || $this->headersSent) {
            return $this;
        }

        if(!isset($this->headers['Date'])) $this->setDate(\DateTime::createFromFormat('U',(string)time()));

        $this->resp->status($this->getStatusCode(), $this->getReasonPhrase());
        
        foreach(array_keys($this->getHeaders()) AS $name){
            $this->resp->header($name,$this->getHeaderLine($name));
        }

        $this->headersSent=TRUE;

        return $this;
    }

    public function sendBody()
    {
        if($this->resp->isWritable()) $this->resp->end($this->body);
        return $this;
    }

    /**
     * Perform a redirect to a new URL, in two flavors: header or location.
     *
     * @param string $uri  The URI to redirect to
     * @param int    $code The type of redirection, defaults to 302
     *
     * @throws HTTPException For invalid status code.
     *
     * @return $this
     */
    public function redirect(string $uri, string $method = 'auto', ?int $code = null)
    {
        // Assume 302 status code response; override if needed
        if (empty($code)) {
            $code = 302;
        }

        // IIS environment likely? Use 'refresh' for better compatibility
        if ($method === 'auto' && isset($this->rqst->server['server_software']) && strpos($this->rqst->server['server_software'], 'Microsoft-IIS') !== false) {
            $method = 'refresh';
        }

        // override status code for HTTP/1.1 & higher
        // reference: http://en.wikipedia.org/wiki/Post/Redirect/Get
        if (isset($this->rqst->server['server_protocol']) && $this->getProtocolVersion() >= 1.1 && $method !== 'refresh') {
            $code = ($this->rqst->getMethod() !== 'GET') ? 303 : ($code === 302 ? 307 : $code);
        }

        switch ($method) {
            case 'refresh':
                $this->setHeader('Refresh', '0;url=' . $uri);
                $this->setStatusCode($code);
                break;

            default:
                $this->resp->redirect($uri,$code);
                break;
        }

        return $this;
    }
}

Session

相比请求和响应,session的处理还要复杂一些。PHP原生提供的session是个完全面向过程的东西,启动时调用session_set_save_handler方法注册钩子,然后PHP就会自动在相应节点调用钩子,完成数据的读删存。
我的办法是用魔术方法替代钩子,用类属性替代全局变量$_SESSION:

<?php namespace App\Swoole\Session;
use CodeIgniter\Cookie\Cookie;
use CodeIgniter\I18n\Time;
class Session extends \CodeIgniter\Session\Session {
    private SerializeHandlerInterface $serializeHandler;
    public function __construct(private \Config\App $config)
    {
        $handlerClass=__NAMESPACE__.'\\SerializeHandler_'.ini_get('session.serialize_handler');
        $this->serializeHandler=new $handlerClass();

        $session = config('Session');
        $this->sessionDriverName        = $session->driver;
        if ($this->sessionDriverName===\CodeIgniter\Session\Handlers\DatabaseHandler::class) {
            $DBGroup = $session->DBGroup ?? config(\Config\Database::class)->defaultGroup;
            $driver = \Config\Database::connect($DBGroup,FALSE)->getPlatform();

            if ($driver === 'MySQLi') {
                $this->sessionDriverName = \CodeIgniter\Session\Handlers\Database\MySQLiHandler::class;
            } elseif ($driver === 'Postgre') {
                $this->sessionDriverName = \CodeIgniter\Session\Handlers\Database\PostgreHandler::class;
            }
        }
        $this->sessionCookieName        = $session->cookieName ?? $this->sessionCookieName;
        $this->sessionExpiration        = $session->expiration ?? $this->sessionExpiration;
        $this->sessionSavePath          = $session->savePath;
        $this->sessionMatchIP           = $session->matchIP ?? $this->sessionMatchIP;
        $this->sessionTimeToUpdate      = $session->timeToUpdate ?? $this->sessionTimeToUpdate;
        $this->sessionRegenerateDestroy = $session->regenerateDestroy ?? $this->sessionRegenerateDestroy;
        

        /** @var CookieConfig $cookie */
        $cookie = config('Cookie');

        $this->cookie = (new Cookie($this->sessionCookieName, '', [
            'expires'  => $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration,
            'path'     => $cookie->path ?? $config->cookiePath,
            'domain'   => $cookie->domain ?? $config->cookieDomain,
            'secure'   => $cookie->secure ?? $config->cookieSecure,
            'httponly' => true, // for security
            'samesite' => $cookie->samesite ?? $config->cookieSameSite ?? Cookie::SAMESITE_LAX,
            'raw'      => $cookie->raw ?? false,
        ]))->withPrefix(''); // Cookie prefix should be ignored.

        helper('array');
        $this->configure();
    }

    
    private \App\Swoole\HTTP\IncomingRequest $request;
    private \App\Swoole\HTTP\Response $response;
    private array $data=[];
    private string $id;
    public function create(
        \App\Swoole\HTTP\IncomingRequest $request,
        \App\Swoole\HTTP\Response $response,
        \Psr\Log\LoggerInterface $logger
        ) : self
    {
        $res=clone $this;
        $res->request=$request;
        $res->response=$response;
        $res->setLogger($logger);
        $className=$this->sessionDriverName;
        $res->driver=new $className($this->config,$request->getIPAddress());
        $res->driver->setLogger($logger);
        return $res->start();
    }
    public function start()
    {
        // Sanitize the cookie, because apparently PHP doesn't do that for userspace handlers
        $cookie=$this->request->getCookie($this->sessionCookieName);
        if(!is_string($cookie)||!preg_match('#\A'.$this->sidRegexp.'\z#',$cookie)){
            $this->id=session_create_id();
            $this->setCookie();
        }else $this->id=$cookie;

        if (ENVIRONMENT !== 'testing'){
            // session_start();
            $this->driver->open($this->sessionSavePath,$this->sessionCookieName);
            $data=$this->driver->read($this->id);
            if($data) $this->data=$this->serializeHandler->unserialize($data);
        }

        // Is session ID auto-regeneration configured? (ignoring ajax requests)
        if (!$this->request->isAJAX()&&$this->sessionTimeToUpdate>0) {
            if (! isset($this->data['__ci_last_regenerate'])) {
                $this->data['__ci_last_regenerate'] = Time::now()->getTimestamp();
            } elseif ($this->data['__ci_last_regenerate']+$this->sessionTimeToUpdate<Time::now()->getTimestamp()){
                $this->regenerate((bool) $this->sessionRegenerateDestroy);
            }
        }
        // Another work-around ... PHP doesn't seem to send the session cookie
        // unless it is being currently created or regenerated
        elseif ($cookie===$this->id) {
            $this->setCookie();
        }

        $this->initVars();
        $this->logger->info("Session: Class initialized using '" . $this->sessionDriverName . "' driver.");

        return $this;
    }
    protected function setCookie()
    {
        $expiration   = $this->sessionExpiration === 0 ? 0 : Time::now()->getTimestamp() + $this->sessionExpiration;
        $this->cookie = $this->cookie->withValue($this->id)->withExpires($expiration);
        $this->response->setCookie($this->cookie);
    }


    public function regenerate(bool $destroy = false)
    {
        $this->data['__ci_last_regenerate'] = Time::now()->getTimestamp();
        if($destroy) $this->driver->destroy($this->id);
        $this->removeOldSessionCookie();
        $this->id=session_create_id();
        if($destroy) $this->driver->read($this->id);
        $this->setCookie();
    }
    private function removeOldSessionCookie(): void
    {
        $cookieStoreInResponse = $this->response->getCookieStore();

        if (! $cookieStoreInResponse->has($this->sessionCookieName)) {
            return;
        }

        // CookieStore is immutable.
        $newCookieStore = $cookieStoreInResponse->remove($this->sessionCookieName);

        // But clear() method clears cookies in the object (not immutable).
        $cookieStoreInResponse->clear();

        foreach ($newCookieStore as $cookie) {
            $this->response->setCookie($cookie);
        }
    }

    public function __destruct()
    {
        if(!isset($this->driver)) return;
        if($this->id) $this->driver->write($this->id,$this->serializeHandler->serialize($this->data));
        $this->driver->close();
    }


    public function stop()
    {
        $this->response->resp->cookie(
            $this->sessionCookieName,
            $this->id,
            ['expires' => 1, 'path' => $this->cookie->getPath(), 'domain' => $this->cookie->getDomain(), 'secure' => $this->cookie->isSecure(), 'httponly' => true]
        );

        $this->driver->destroy($this->id);
        $this->id='';
    }
    public function destroy()
    {
        if (ENVIRONMENT!=='testing') $this->stop();
    }

    /**
     * Handle temporary variables
     *
     * Clears old "flash" data, marks the new one for deletion and handles
     * "temp" data deletion.
     */
    protected function initVars()
    {
        if (empty($this->data['__ci_vars'])) {
            return;
        }

        $currentTime = Time::now()->getTimestamp();

        foreach ($this->data['__ci_vars'] as $key => &$value) {
            if ($value === 'new') {
                $this->data['__ci_vars'][$key] = 'old';
            }
            // DO NOT move this above the 'new' check!
            elseif ($value === 'old' || $value < $currentTime) {
                unset($this->data[$key], $this->data['__ci_vars'][$key]);
            }
        }

        if (empty($this->data['__ci_vars'])) {
            unset($this->data['__ci_vars']);
        }
    }


    public function set($data, $value = null)
    {
        if (is_array($data)) {
            foreach ($data as $key => &$value) {
                if (is_int($key)) {
                    $this->data[$value] = null;
                } else {
                    $this->data[$key] = $value;
                }
            }

            return;
        }

        $this->data[$data] = $value;
    }
    public function get(?string $key = null)
    {
        if (! empty($key) && (null !== ($value = $this->data[$key] ?? null) || null !== ($value = dot_array_search($key, $this->data ?? [])))) {
            return $value;
        }

        if (empty($this->data)) {
            return $key === null ? [] : null;
        }

        if (! empty($key)) {
            return null;
        }

        $userdata = [];
        $_exclude = array_merge(['__ci_vars'], $this->getFlashKeys(), $this->getTempKeys());

        $keys = array_keys($this->data);

        foreach ($keys as $key) {
            if (! in_array($key, $_exclude, true)) {
                $userdata[$key] = $this->data[$key];
            }
        }

        return $userdata;
    }
    public function has(string $key): bool
    {
        return isset($this->data[$key]);
    }
    public function remove($key)
    {
        if (is_array($key)) {
            foreach ($key as $k) {
                unset($this->data[$k]);
            }

            return;
        }

        unset($this->data[$key]);
    }

    public function __set(string $key, $value)
    {
        $this->data[$key] = $value;
    }
    public function __get(string $key)
    {
        // Note: Keep this order the same, just in case somebody wants to
        // use 'session_id' as a session data key, for whatever reason
        if (isset($this->data[$key])) {
            return $this->data[$key];
        }

        if ($key === 'session_id') {
            return $this->id;
        }

        return null;
    }
    public function __isset(string $key): bool
    {
        return isset($this->data[$key]) || ($key === 'session_id');
    }


    /**
     * Retrieve one or more items of flash data from the session.
     *
     * If the item key is null, return all flashdata.
     *
     * @param string $key Property identifier
     *
     * @return array|null The requested property value, or an associative array  of them
     */
    public function getFlashdata(?string $key = null)
    {
        if (isset($key)) {
            return (isset($this->data['__ci_vars'], $this->data['__ci_vars'][$key], $this->data[$key])
                && ! is_int($this->data['__ci_vars'][$key])) ? $this->data[$key] : null;
        }

        $flashdata = [];

        if (! empty($this->data['__ci_vars'])) {
            foreach ($this->data['__ci_vars'] as $key => &$value) {
                if (! is_int($value)) {
                    $flashdata[$key] = $this->data[$key];
                }
            }
        }

        return $flashdata;
    }


    /**
     * Mark a session property or properties as flashdata.
     *
     * @param array|string $key Property identifier or array of them
     *
     * @return bool False if any of the properties are not already set
     */
    public function markAsFlashdata($key): bool
    {
        if (is_array($key)) {
            foreach ($key as $sessionKey) {
                if (! isset($this->data[$sessionKey])) {
                    return false;
                }
            }

            $new = array_fill_keys($key, 'new');

            $this->data['__ci_vars'] = isset($this->data['__ci_vars']) ? array_merge($this->data['__ci_vars'], $new) : $new;

            return true;
        }

        if (! isset($this->data[$key])) {
            return false;
        }

        $this->data['__ci_vars'][$key] = 'new';

        return true;
    }

    /**
     * Unmark data in the session as flashdata.
     *
     * @param mixed $key Property identifier or array of them
     */
    public function unmarkFlashdata($key)
    {
        if (empty($this->data['__ci_vars'])) {
            return;
        }

        if (! is_array($key)) {
            $key = [$key];
        }

        foreach ($key as $k) {
            if (isset($this->data['__ci_vars'][$k]) && ! is_int($this->data['__ci_vars'][$k])) {
                unset($this->data['__ci_vars'][$k]);
            }
        }

        if (empty($this->data['__ci_vars'])) {
            unset($this->data['__ci_vars']);
        }
    }

    /**
     * Retrieve all of the keys for session data marked as flashdata.
     *
     * @return array The property names of all flashdata
     */
    public function getFlashKeys(): array
    {
        if (! isset($this->data['__ci_vars'])) {
            return [];
        }

        $keys = [];

        foreach (array_keys($this->data['__ci_vars']) as $key) {
            if (! is_int($this->data['__ci_vars'][$key])) {
                $keys[] = $key;
            }
        }

        return $keys;
    }


    /**
     * Returns either a single piece of tempdata, or all temp data currently
     * in the session.
     *
     * @param string $key Session data key
     *
     * @return mixed Session data value or null if not found.
     */
    public function getTempdata(?string $key = null)
    {
        if (isset($key)) {
            return (isset($this->data['__ci_vars'], $this->data['__ci_vars'][$key], $this->data[$key])
                    && is_int($this->data['__ci_vars'][$key])) ? $this->data[$key] : null;
        }

        $tempdata = [];

        if (! empty($this->data['__ci_vars'])) {
            foreach ($this->data['__ci_vars'] as $key => &$value) {
                if (is_int($value)) {
                    $tempdata[$key] = $this->data[$key];
                }
            }
        }

        return $tempdata;
    }

    /**
     * Removes a single piece of temporary data from the session.
     *
     * @param string $key Session data key
     */
    public function removeTempdata(string $key)
    {
        $this->unmarkTempdata($key);
        unset($this->data[$key]);
    }

    /**
     * Mark one of more pieces of data as being temporary, meaning that
     * it has a set lifespan within the session.
     *
     * @param array|string $key Property identifier or array of them
     * @param int          $ttl Time to live, in seconds
     *
     * @return bool False if any of the properties were not set
     */
    public function markAsTempdata($key, int $ttl = 300): bool
    {
        $ttl += Time::now()->getTimestamp();

        if (is_array($key)) {
            $temp = [];

            foreach ($key as $k => $v) {
                // Do we have a key => ttl pair, or just a key?
                if (is_int($k)) {
                    $k = $v;
                    $v = $ttl;
                } elseif (is_string($v)) {
                    $v = Time::now()->getTimestamp() + $ttl;
                } else {
                    $v += Time::now()->getTimestamp();
                }

                if (! array_key_exists($k, $this->data)) {
                    return false;
                }

                $temp[$k] = $v;
            }

            $this->data['__ci_vars'] = isset($this->data['__ci_vars']) ? array_merge($this->data['__ci_vars'], $temp) : $temp;

            return true;
        }

        if (! isset($this->data[$key])) {
            return false;
        }

        $this->data['__ci_vars'][$key] = $ttl;

        return true;
    }

    /**
     * Unmarks temporary data in the session, effectively removing its
     * lifespan and allowing it to live as long as the session does.
     *
     * @param array|string $key Property identifier or array of them
     */
    public function unmarkTempdata($key)
    {
        if (empty($this->data['__ci_vars'])) {
            return;
        }

        if (! is_array($key)) {
            $key = [$key];
        }

        foreach ($key as $k) {
            if (isset($this->data['__ci_vars'][$k]) && is_int($this->data['__ci_vars'][$k])) {
                unset($this->data['__ci_vars'][$k]);
            }
        }

        if (empty($this->data['__ci_vars'])) {
            unset($this->data['__ci_vars']);
        }
    }

    /**
     * Retrieve the keys of all session data that have been marked as temporary data.
     */
    public function getTempKeys(): array
    {
        if (! isset($this->data['__ci_vars'])) {
            return [];
        }

        $keys = [];

        foreach (array_keys($this->data['__ci_vars']) as $key) {
            if (is_int($this->data['__ci_vars'][$key])) {
                $keys[] = $key;
            }
        }

        return $keys;
    }
}

其中,由于底层对数据序列化的处理句柄(session.serialize_handler,主要是php)没有现成的替代方案,索性额外写个接口并实现:

/app/Swoole/Session/SerializeHandlerInterface.php
<?php namespace App\Swoole\Session;
interface SerializeHandlerInterface {
    public function serialize(array $data) : string;
    public function unserialize(string $data) : array;
}

/app/Swoole/Session/SerializeHandler_php.php
<?php namespace App\Swoole\Session;
class SerializeHandler_php implements SerializeHandlerInterface {
    public function serialize(array $data) : string
    {
        $res='';
        foreach($data AS $k => $v) $res.=$k.'|'.serialize($v);
        return $res;
    }

    public function unserialize(string $data) : array
    {
        $parts=explode('|',$data);
        $end=count($parts)-1;
        $keys=[$parts[0]];
        for($i=1;$i!==$end;$i++){
            $len=strrpos($parts[$i],';')+1;
            if($parts[$i][$len]==='}') $len++;
            $values[]=substr($parts[$i],0,$len);
            $keys[]=substr($parts[$i],$len);
        }
        $values[]=$parts[$end];
        foreach($keys AS $i => $k){
            $res[$k]=unserialize($values[$i]);
        }
        return $res;
    }
}

另外,框架中有些地方调用了PHP提供的公共函数setcookie等对客户端的cookie进行处理。虽然函数不能覆盖,但PHP调用函数是先在本命名空间查找的,而不同命名空间下是可以存在同名函数的。利用这一特点,我们只要在调用函数的命名空间下定义同名函数,就可以劫持调用:

<?php
namespace CodeIgniter\Cookie {
    function setcookie(string $name, string $value, array $options)
    {
        // 很遗憾swoole的cookie方法和PHP的setcookie没有完全一致,不支持$options传参,以后如果支持了可以改用下面一行,与框架保持完全一致
        return getRootContext()->response->resp->cookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
        // getRootContext()->response->resp->cookie($name, $value, $options);
    }
    function setrawcookie(string $name, string $value, array $options)
    {
        return getRootContext()->response->resp->rawCookie($name, $value, $options['expires'], $options['path'], $options['domain'], $options['secure'], $options['httponly']);
        // getRootContext()->response->resp->rawCookie($name, $value, $options);
    }
}
namespace CodeIgniter\HTTP {
    function setcookie(...$args)
    {
        return \CodeIgniter\Cookie\setcookie(...$args);
    }
    function setrawcookie(...$args)
    {
        return \CodeIgniter\Cookie\setrawcookie(...$args);
    }
}
namespace CodeIgniter\Session\Handlers {
    function setcookie(...$args)
    {
        return \CodeIgniter\Cookie\setcookie(...$args);
    }
    function setrawcookie(...$args)
    {
        return \CodeIgniter\Cookie\setrawcookie(...$args);
    }
    function ini_set(...$args)
    {
    }
}

Exceptions

前面四个组件的改造,纯FPM框架应该都是需要的,而异常、错误的处理则要看具体框架。如果你想改造的框架也是通过set_exception_handler等方法注册全局回调,那么也需要像我这样改成手动调用相应方法进行处理。
首先要梳理框架处理异常的逻辑。CI4中有一个CodeIgniter\Debug\Exceptions类,类本身存了一些异常处理的配置,然后外部会调用initialize方法进行全局注册,这样当有未捕获的异常抛出时,就会触发注册的回调。因为回调就是这个类的方法,所以我的办法就是在抛出异常的时候实例化一个异常处理对象,然后主动调用处理方法。
当然有一小部分内容还是需要调整的:

/app/Swoole/Exceptions.php
<?php namespace App\Swoole;
class Exceptions extends \CodeIgniter\Debug\Exceptions {
    public function __construct(HTTP\IncomingRequest $request,HTTP\Response $response)
    {
        $this->config   = self::$configs;
        $this->viewPath = rtrim($this->config->errorViewPath,'\\/ ').DIRECTORY_SEPARATOR;
        $this->request  = $request;
        $this->response = $response;
    }
    
    public function exceptionHandler(\Throwable $exception)
    {
        [$statusCode, $exitCode] = $this->determineCodes($exception);

        if($this->config->log===true&&!in_array($statusCode,$this->config->ignoreCodes,true)) logThrowable($exception);

        try {
            $this->response->setStatusCode($statusCode);
        } catch (\CodeIgniter\HTTP\Exceptions\HTTPException $e) {
            // Workaround for invalid HTTP status code.
            $statusCode = 500;
            $this->response->setStatusCode($statusCode);
        }

        if (strpos($this->request->getHeaderLine('accept'), 'text/html') === false) {
            $this->respond(ENVIRONMENT === 'development' ? $this->collectVars($exception, $statusCode) : '', $statusCode)->send();
        }else $this->render($exception, $statusCode);
    }
    
    protected function render(\Throwable $exception, int $statusCode)
    {
        // Determine possible directories of error views
        $path    = $this->viewPath.'html'.DIRECTORY_SEPARATOR;
        $altPath = rtrim((new \Config\Paths())->viewDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'errors' . DIRECTORY_SEPARATOR.'html'.DIRECTORY_SEPARATOR;

        // Determine the views
        $view    = $this->determineView($exception, $path);
        $altView = $this->determineView($exception, $altPath);

        // Check if the view exists
        if (is_file($path . $view)) {
            $viewFile = $path . $view;
        } elseif (is_file($altPath . $altView)) {
            $viewFile = $altPath . $altView;
        } else {
            $this->response->setBody('The error view files were not found. Cannot render exception trace.')->send();
            return;
        }

        $this->response->setBody((function () use ($exception, $statusCode, $viewFile): string {
            $vars = $this->collectVars($exception, $statusCode);
            $vars['response']=$this->response;
            $vars['request']=$this->request;
            extract($vars, EXTR_SKIP);

            ob_start();
            include $viewFile;

            return ob_get_clean();
        })())->send();
    }

    
    protected function determineView(\Throwable $exception, string $templatePath): string
    {
        // Production environments should have a custom exception file.
        $view         = 'production.php';
        $templatePath = rtrim($templatePath, '\\/ ') . DIRECTORY_SEPARATOR;

        if (str_ireplace(['off', 'none', 'no', 'false', 'null'], '', ini_get('display_errors'))) {
            $view = 'error_exception_swoole.php';
        }

        // 404 Errors
        if ($exception instanceof CodeIgniter\Exceptions\PageNotFoundException) {
            return 'error_404.php';
        }

        // Allow for custom views based upon the status code
        if (is_file($templatePath . 'error_' . $exception->getCode() . '.php')) {
            return 'error_' . $exception->getCode() . '.php';
        }

        return $view;
    }
    
    public static $configs;
}
Exceptions::$configs=config('Exceptions');
if (! isset(Exceptions::$configs->sensitiveDataInTrace)) {
    Exceptions::$configs->sensitiveDataInTrace = [];
}
if (! isset(Exceptions::$configs->logDeprecations, Exceptions::$configs->deprecationLogLevel)) {
    Exceptions::$configs->logDeprecations     = false;
    Exceptions::$configs->deprecationLogLevel = \Psr\Log\LogLevel::WARNING;
}

添加到/app/Common.php中
function logThrowable(Throwable $th,string $level='critical',string $prepend='')
{
	$backtraces = [];
	foreach ($th->getTrace() as $index => $trace) {
		$frame = $trace + ['file' => '[internal function]', 'line' => '', 'class' => '', 'type' => '', 'args' => []];

		if ($frame['file'] !== '[internal function]') {
			$frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']);
		}

		unset($frame['line']);
		$idx = $index;
		$idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT);

		$args = implode(', ', array_map(static function ($value): string {
			switch (true) {
				case is_object($value):
					return sprintf('Object(%s)', get_class($value));

				case is_array($value):
					return $value !== [] ? '[...]' : '[]';

				case $value === null:
					return 'null';

				case is_resource($value):
					return sprintf('resource (%s)', get_resource_type($value));

				case is_string($value):
					return var_export(clean_path($value), true);

				default:
					return var_export($value, true);
			}
		}, $frame['args']));

		$backtraces[] = sprintf(
			'%s %s: %s%s%s(%s)',
			$idx,
			clean_path($frame['file']),
			$frame['class'],
			$frame['type'],
			$frame['function'],
			$args
		);
	}

	log_message($level,$prepend."{message}\nin {exFile} on line {exLine}.\n{trace}", [
		'message' => $th->getMessage(),
		'exFile'  => clean_path($th->getFile()), // {file} refers to THIS file
		'exLine'  => $th->getLine(), // {line} refers to THIS line
		'trace'   => implode("\n", $backtraces),
	]);
}

运行

$app生命周期

至此,适配swoole需要的组件改造已搞定,接下来就是循着FPM模式下一个请求的生命周期,进行代码移植。需要注意的是,使用swoole通常还会用到它的websocket功能,因此一般都是以多进程模式运行。
在多进程模式下,开发者手动启动一个主进程,这个主进程里加载到内存的代码是可以在worker进程中访问的,而worker进程里加载的代码却是不能被其他worker进程访问的,并且会随着进程退出而全部释放。因此在下方代码中,我注册了一个WorkerExit事件,在回调里清空定时器,因为定时器在终止前会一直占着内存,导致worker进程无法正常退出。
CI框架的进程周期是从/public/index.php开始的,因此下面这个文件的代码就是从index.php开始,按照执行顺序逐一复制过来,并根据swoole进程的特点进行微调,一些关键的组件改用前面改造后的。
需要特别注意的是,只有那些启动后不会改变,或者即使改变了也不会出现协程间冲突的内容可以加载到主进程中,例如$sessionFactory。

<?php
<?php

function getRootContext()
{
    $pcid=Swoole\Coroutine::getPcid();
    while($pCtx=Swoole\Coroutine::getContext($pcid)){
        $ctx=$pCtx;
        $pcid=Swoole\Coroutine::getPcid($pcid);
    }
    return $ctx??Swoole\Coroutine::getContext();
}
function session(?string $val=null)
{
    global $sessionFactory;
    $ctx=getRootContext();
    if(!isset($ctx->session)) $ctx->session=$sessionFactory->create($ctx->request,$ctx->response,$ctx->logger);
    return is_string($val)?$ctx->session->get($val):$ctx->session;
}
function model(string $name, bool $getShared = true, ?CodeIgniter\Database\ConnectionInterface &$conn = null)
{
    if(!$getShared) return CodeIgniter\Config\Factories::models($name, ['getShared' => FALSE], $conn);
    $rootContext=getRootContext();
    if(isset($rootContext[$name])) return $rootContext[$name];
    return $rootContext[$name]=CodeIgniter\Config\Factories::models($name, ['getShared' => FALSE], $conn);
}

class Connection {
    private function __construct(
        private readonly Swoole\WebSocket\Server $server,
        private readonly int $fd
    )
    {
    }

    // push/exist/disconnect/isEstablished
    public function __call(string $name,array $arguments)
    {
        return $this->server->$name($this->fd,...$arguments);
    }

    public function remove()
    {
        return self::$table->del($this->fd);
    }


    // public修饰是便于初始化,不要在其他任何地方直接访问$table!
    public static Swoole\Table $table;
    public static function __callStatic(string $name,array $arguments)
    {
        return self::$table->$name(...$arguments);
    }
    public static function get(Swoole\WebSocket\Server $ws,string $fd) : ?self
    {
        $data=self::$table->get($fd);
        if(!$data) return NULL;
        $res=new self($ws,$fd);
        foreach($data AS $k => $v) $res->$k=$v;
        return $res;
    }

    public static function set(string $fd,int $loginId)
    {
        self::$table->set($fd,['login_id'=>$loginId]);
    }

    public function update(array $data)
    {
        foreach($data AS $k => $v) $this->$k=$v;
        self::$table->set($this->fd,[
            'login_id'=>$this->login_id,
            'chat_type'=>$this->chat_type,
            'chat_id'=>$this->chat_id
        ]);
    }
    private int $login_id;
    public int $chat_type;
    public int $chat_id;
}
Connection::$table=new Swoole\Table(2048); // worker_num × max_request
Connection::$table->column('login_id',Swoole\Table::TYPE_INT);
Connection::$table->column('chat_type',Swoole\Table::TYPE_INT);
Connection::$table->column('chat_id',Swoole\Table::TYPE_INT);
Connection::$table->create();

define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR.'public'.DIRECTORY_SEPARATOR);

// Ensure the current directory is pointing to the front controller's directory
chdir(FCPATH);

/*
 *---------------------------------------------------------------
 * BOOTSTRAP THE APPLICATION
 *---------------------------------------------------------------
 * This process sets up the path constants, loads and registers
 * our autoloader, along with Composer's, loads our constants
 * and fires up an environment-specific bootstrapping.
 */

// Load our paths config file
// This is the line that might need to be changed, depending on your folder structure.
require FCPATH . '../app/Config/Paths.php';

$paths = new Config\Paths();

// Location of the framework bootstrap file.
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';


// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
if (! defined('ENVIRONMENT')) {
    define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
}
if (is_file(APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php')) {
    require_once APPPATH . 'Config/Boot/' . ENVIRONMENT . '.php';
} else {
    // @codeCoverageIgnoreStart
    echo 'The application environment is not set correctly.';

    exit(EXIT_ERROR); // EXIT_ERROR
    // @codeCoverageIgnoreEnd
}
require_once __DIR__.DIRECTORY_SEPARATOR.'override.php';

use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Router\Exceptions\RedirectException;
use Config\Kint as KintConfig;
use Config\Services;
use Kint\Renderer\CliRenderer;
use Kint\Renderer\RichRenderer;
use CodeIgniter\Filters\Filters;

if (CI_DEBUG){
    if (! defined('KINT_DIR')) {
        spl_autoload_register(function ($class) {
            $class = explode('\\', $class);

            if (array_shift($class) !== 'Kint') {
                return;
            }

            $file = SYSTEMPATH . 'ThirdParty/Kint/' . implode('/', $class) . '.php';

            if (is_file($file)) {
                require_once $file;
            }
        });

        require_once SYSTEMPATH . 'ThirdParty/Kint/init.php';
    }
    $kintConfig = config(KintConfig::class);

    Kint::$depth_limit         = $kintConfig->maxDepth;
    Kint::$display_called_from = $kintConfig->displayCalledFrom;
    Kint::$expanded            = $kintConfig->expanded;

    if (! empty($kintConfig->plugins) && is_array($kintConfig->plugins)) {
        Kint::$plugins = $kintConfig->plugins;
    }

    $csp = Services::csp();
    if ($csp->enabled()) {
        RichRenderer::$js_nonce  = $csp->getScriptNonce();
        RichRenderer::$css_nonce = $csp->getStyleNonce();
    }

    RichRenderer::$theme  = $kintConfig->richTheme;
    RichRenderer::$folder = $kintConfig->richFolder;
    RichRenderer::$sort   = $kintConfig->richSort;
    if (! empty($kintConfig->richObjectPlugins) && is_array($kintConfig->richObjectPlugins)) {
        RichRenderer::$value_plugins = $kintConfig->richObjectPlugins;
    }
    if (! empty($kintConfig->richTabPlugins) && is_array($kintConfig->richTabPlugins)) {
        RichRenderer::$tab_plugins = $kintConfig->richTabPlugins;
    }

    CliRenderer::$cli_colors         = $kintConfig->cliColors;
    CliRenderer::$force_utf8         = $kintConfig->cliForceUTF8;
    CliRenderer::$detect_width       = $kintConfig->cliDetectWidth;
    CliRenderer::$min_terminal_width = $kintConfig->cliMinWidth;
} elseif (class_exists(Kint::class)) {
    // In case that Kint is already loaded via Composer.
    Kint::$enabled_mode = false;
    // @codeCoverageIgnore
}
helper('kint');

$config = config('App');
Locale::setDefault($config->defaultLocale ?? 'en');
date_default_timezone_set($config->appTimezone ?? 'UTC');

define('DB_POOL_SIZE',32);
/**
 * 文档介绍$mode默认值是SWOOLE_PROCESS(https://wiki.swoole.com/#/server/methods?id=__construct)
 * 但经测试似乎并不是:
 * 如果不传入SWOOLE_PROCESS,WorkerExit后会关闭websocket连接,并且是在回调执行后关闭。
 * 猜测底层是在回调执行后立即销毁协程环境,导致无法执行数据库操作,进而引发死锁。
 * 因此目前SWOOLE_PROCESS值必须传入
 */
$server=new Swoole\WebSocket\Server('0.0.0.0',80,SWOOLE_PROCESS);
$server->on('WorkerExit',function(){
    Swoole\Timer::clearAll();
});

$routes = Services::routes(FALSE);
$routes->setDefaultNamespace('App\Swoole\Controllers');
$routes->set404Override();
$routes->get('/swoole(/(.+))?', 'Swoole::$2');

$filtersConfig=config('Filters');
if(empty(config('Feature')->multipleFilters)){
    function getFilters($router,$rqst, $resp)
    {
        global $filtersConfig;
        $filters=new Filters($filtersConfig, $rqst, $resp);
        if($routeFilter=$router->getFilter()){
            $filters->enableFilter($routeFilter, 'before');
            $filters->enableFilter($routeFilter, 'after');
        }
        return $filters;
    }
}else{
    function getFilters($router,$rqst, $resp)
    {
        global $filtersConfig;
        $filters=new Filters($filtersConfig, $rqst, $resp);
        if($routeFilter=$router->getFilters()){
            $filters->enableFilters($routeFilter, 'before');
            $filters->enableFilters($routeFilter, 'after');
        }
        return $filters;
    }
}

$sessionFactory=new App\Swoole\Session\Session($config);
$server->on('request',function($request,$response)use(&$config,&$routes){
    $startTime = microtime(true);
    $benchmark = Services::timer(FALSE);
    $benchmark->start('total_execution', $startTime);
    $benchmark->start('bootstrap');

    $rqst=new App\Swoole\HTTP\IncomingRequest($config,$request);
    if (strtolower($rqst->getMethod())==='post'&&($method=$rqst->getPost('_method'))&&in_array(strtoupper($method), ['PUT', 'PATCH', 'DELETE'], true)) $rqst->setMethod($method);
    $resp=new App\Swoole\HTTP\Response($config,$response,$request);
    $resp->setProtocolVersion($rqst->getProtocolVersion());

    $router = Services::router($routes, $rqst,FALSE);

    $path=$rqst->getPath();

    $benchmark->stop('bootstrap');
    $benchmark->start('routing');

    $ctx=getRootContext();
    $ctx->request=$rqst;
    $ctx->response=$resp;
    $ctx->logger=Services::logger(FALSE);
    try {
        $controller = $router->handle($path);
        $method     = $router->methodName();

        if ($router->hasLocale()) {
            $rqst->setLocale($router->getLocale());
        }

        $benchmark->stop('routing');
        $filters=getFilters($router,$rqst, $resp);
        $benchmark->start('before_filters');
        $possibleResponse = $filters->run($path, 'before');
        $benchmark->stop('before_filters');
        if ($possibleResponse instanceof ResponseInterface) {
            return $possibleResponse->send();
        }

        $benchmark->start('controller');
        $benchmark->start('controller_constructor');

        // Is it routed to a Closure?
        if ($controller instanceof Closure) {
            $returned = $controller(...$router->params());
            $benchmark->stop('controller_constructor');
            $benchmark->stop('controller');
        }elseif (empty($controller)) {
            throw PageNotFoundException::forEmptyController();
        }elseif (! class_exists($controller, true) || $method[0] === '_') {
            throw PageNotFoundException::forControllerNotFound($controller, $method);
        }else {
            $class = new $controller();
            $class->initController($rqst, $resp,$ctx->logger);

            $benchmark->stop('controller_constructor');

            if (! method_exists($class, '_remap') && ! is_callable([$class, $method], false)) {
                throw PageNotFoundException::forMethodNotFound($method);
            }

            // Is there a "post_controller_constructor" event?
            Events::trigger('post_controller_constructor');

            $params = $router->params();

            $returned = method_exists($class, '_remap')
                ? $class->_remap($method, ...$params)
                : $class->{$method}(...$params);

            $benchmark->stop('controller');
        }

        if ($returned instanceof ResponseInterface) $resp=$returned;
        elseif(is_string($returned)) $resp->setBody($returned);

        $filters->setResponse($resp);
        $totalTime = $benchmark->getElapsedTime('total_execution');

        // Run "after" filters
        $benchmark->start('after_filters');
        $returned = $filters->run($path, 'after');
        $benchmark->stop('after_filters');

        if ($returned instanceof ResponseInterface) {
            $resp = $returned;
        }

        if (
            ! $resp instanceof DownloadResponse
            && ! $resp instanceof RedirectResponse
        ) {

            // Update the performance metrics
            $body = $resp->getBody();
            if ($body !== null) {
                $output = str_replace('0.0776', (string) $totalTime, $body);
                $resp->setBody($output);
            }
        }

        unset($path);

        $resp->send();
    } catch (RedirectException $e) {
        $ctx->logger->info('REDIRECTED ROUTE at ' . $e->getMessage());

        // If the route is a 'redirect' route, it throws
        // the exception with the $to as the message
        $resp->redirect(base_url($e->getMessage()), 'auto', $e->getCode());
        $resp->send();
        return;
    } catch (PageNotFoundException $e) {
        if ($override = $router->get404Override()) {
            $returned = null;

            if ($override instanceof Closure) {
                $returned=$override($e->getMessage());
            } elseif (is_array($override)) {
                $benchmark->start('controller');
                $benchmark->start('controller_constructor');

                list($controller,$method)=$override;

                $class = new $controller();
                $class->initController($rqst, $resp, $ctx->logger);

                $benchmark->stop('controller_constructor');
                $params = $router->params();

                $returned = method_exists($class, '_remap')
                    ? $class->_remap($method, ...$params)
                    : $class->{$method}(...$params);

                $benchmark->stop('controller');
            }

            unset($override);

            if ($returned instanceof ResponseInterface) $resp=$returned;
            elseif(is_string($returned)) $resp->setBody($returned);
            $resp->send();
            return;
        }
        $exceptions=new App\Swoole\Exceptions($rqst,$resp);
        $exceptions->exceptionHandler(PageNotFoundException::forPageNotFound(
            (ENVIRONMENT !== 'production') ? $e->getMessage() : null
        ));
    } catch (\Throwable $th) {
        $exceptions=new App\Swoole\Exceptions($rqst,$resp);
        $exceptions->exceptionHandler($th);
    }
});

$server->on('open',function($ws,$request)use(&$config){
    $rqst=new App\Swoole\HTTP\IncomingRequest($config,$request);
    if(!$rqst->getGet('token')){
        // 其他处理
        echo 'open:',$request->fd,'没有token',PHP_EOL;
        return;
    }
    $loginId=mt_rand(0,1023);
    Connection::set($request->fd,$loginId);
    echo 'open:',$request->fd,'有token,loginId是',$loginId,PHP_EOL;
});
$server->on('message',function($ws,$frame){
    if($frame->data===''){ //心跳
        echo '心跳',PHP_EOL;
        $pingFrame=new \Swoole\WebSocket\Frame();
        $pingFrame->opcode=WEBSOCKET_OPCODE_PING;
        $ws->push($frame->fd,$pingFrame);
        return;
    }
    if(!$connection=Connection::get($ws,$frame->fd)){
        echo 'message:没有token的',$frame->fd,PHP_EOL;
        return;
    }
    // 其他处理
    echo 'message:有token的',$frame->fd,PHP_EOL;
    $ws->disconnect($frame->fd);
});
$server->on('close',function($ws,$fd){
    $info=$ws->getClientInfo($fd);
    if(empty($info['websocket_status'])) return;
    if(!$conn=Connection::get($ws,$fd)){
        echo 'closed:没有token的',$fd,PHP_EOL;
        // 其他处理
        return;
    }
    echo 'closed:有token的',$fd,PHP_EOL;
    Swoole\Coroutine::create(function()use($conn){
        $conn->remove();
        // 收尾工作
    });
});

$logFile=WRITEPATH.'logs'.DIRECTORY_SEPARATOR.'swoole.txt';
if(!file_exists($logFile)){
    fclose(fopen($logFile,'w+'));
    chown($logFile,'nobody');
}
$server->set([
    'daemonize'=>ENVIRONMENT!=='development', // 后台运行
    'group'=>'nobody',
    'user'=>'nobody',
    'worker_num'=>2,
    'reload_async'=>TRUE, // 异步事件完成后再退出 Worker 进程
    'max_wait_time'=>60, // 等待异步事件完成的最大时长
    'max_request'=>512, // 每个worker进程处理请求数
    'max_connection'=>10000, // 并发连接数
    // 'enable_deadlock_check'=>FALSE, // 检测协程死锁,默认开启
    'hook_flags'=>SWOOLE_HOOK_NATIVE_CURL|SWOOLE_HOOK_FILE|SWOOLE_HOOK_TCP,
    'open_http2_protocol'=>TRUE,
    'log_level'=>SWOOLE_LOG_WARNING,
    'log_file'=>$logFile,
    'document_root'=>FCPATH,
    'enable_coroutine'=>TRUE,
    'enable_static_handler'=>TRUE, //允许访问静态文件
    'heartbeat_check_interval'=>10, // 心跳间隔
    'heartbeat_idle_time'=>30, // 超时断开
]);
$server->start();

nginx配置

swoole的优势在于轻,处理请求消耗的平均资源远小于FPM,但并不是所有请求都适合用swoole,特别是一些业务层比较重的请求,swoole常驻内存部分所节省的消耗相对业务本身不足一提,那这时候自然是用FPM一步到底划算一些。例如GD库输出图像二进制数据,在swoole中还要用缓冲区函数把内容截下来,再响应给客户端,反而不如直接输出给终端高效。
插一条友链,本人改造的基于GD库的QRcode类,使用超方便:

$logoUri='./path/to/logo.png'; //支持本地和网络资源,如需支持其他图片格式需要在相应位置作调整
$qr=new QRcode('这里是二维码内容');
$qr->png(NULL,$logoUri,20); //第一个参数是要写入的文件名(不含后缀),第三个参数是logo宽占二维码宽的比例
exit;

回归正题,很多时候纯swoole也不能完全满足需求,因此个人推荐用反向代理,一套代码运行两种模式,我的nginx配置server部分如下:

    server {
        listen 443 ssl http2;
        server_name test.cn;
        root /www/test/public/;
        #access_log /www/test/writable/logs/nginx_access.txt;
        error_log /www/test/writable/logs/nginx_error.txt;
        ssl_certificate /path/to.pem;
        ssl_certificate_key /path/to.key;

        # 添加index.php
        location / {
            try_files $uri $uri/ /index.php$is_args$args;
        }
        # PHP-FPM
        location ~ \.php$ {
            include fastcgi_params;
            fastcgi_param HTTPS on;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
            # 指向反代
            fastcgi_pass 127.0.0.1:9000;
            try_files $uri =404;
        }
        # Swoole
        location ~ ^/(swoole|otherRootPath) {
            proxy_pass http://127.0.0.1:80;
            proxy_set_header Host $http_host;
            proxy_set_header X-Forwarded-Host $http_host;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'Upgrade';
            proxy_http_version      1.1;
            proxy_read_timeout 60s;
        }

        # location ~ ^/html {
        #     rewrite ^/html/(.*).html /html/$1/index.html break;
        # }

        location ~* /\. {
            deny all;
        }
    }

注:经测试证明,即使是反向代理,SSL校验也是nginx处理的,因此swoole中不需要配置SSL。但我调试过程中偶现浏览器提示不安全,即SSL校验不成功的问题,还没找到原因。如果你也遇到了,不用在swoole上找原因,我试过不是那个问题。

启动

关于“全局”空间中提到的代码,在$app生命周期中已经有呈现了,而session中提到的覆盖原生方法的代码,就是$app生命周期代码中的这一行:

require_once __DIR__.DIRECTORY_SEPARATOR.'override.php';

$app生命周期的代码文件随便取啥名,但是要和override.php以及框架的spark平级,都在项目根目录下。至于其他框架,自己看情况吧,别让用户访问到,并且要注意路径调整。
因为我的文件名是swoole.php,并且使用的是swoole-cli 5.0.1,因此启动命令为:

swoole-cli -c /usr/local/etc/ /www/test/swoole.php

-c是指定配置所在目录,详见swoole-cli -h
服务启动后,根据nginx的配置,请求swoole路径下的资源都会走swoole进程,反之则走FPM。

总结

作为个人开发者,swoole让我可以用一门PHP语言解决多种需求,并且还是国人自己造的轮子,我真心希望PHPer们都能用上swoole。
去年swoole-cli发布后,连安装都省了(包括PHP包哦!),只要下载下来,放到/usr/bin目录下,并添加可执行权限,就能同时拥有swoole和php(缺陷是只有8.1版并且没有兼容旧版的计划),像我这种喜欢官网下载压缩包后编译安装的人,部署服务器能省不少时间。
因此我思考再三还是决定把这些东西写出来和大家分享,虽然CodeIgniter在国内是比较小众的框架,但纯FPM与swoole间的差异基本就这几点,我想或多或少都能帮到一些。

最后,借用某个swoole交流群里看到的一段话与PHPer们共(zi)勉(wo)一(an)下(wei):
过去三年,绝大部分人都过得很艰难,很多原本有着“宏伟梦想”的企业都倒下了。正因如此,技术负责人们越来越喜欢“够用而且便宜”的技术方案。解释执行的PHP在性能上确实跟Java等编译执行的语言存在不少差距,底层安全问题也是很多高级程序员嫌弃的原因,但其易上手、成本低的优势是不容置疑的。无数历史证明,越简单易用的技术越容易得到普及,我相信PHP的未来一定是上升的,swoole-cli的出现让我更坚信了这点!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值