├── LICENSE.txt ├── README.md ├── adapterman ├── cli-php.ini ├── composer.json ├── recipes ├── README.md ├── bolt.md ├── caddy-config.md ├── drupal.md ├── laravel.md ├── lumen.md ├── nginx-config.md ├── slim.md ├── symfony.md ├── thinkphp-cn.md └── thinkphp.md └── src ├── Adapterman.php ├── Http.php ├── ParseMultipart.php ├── Session.php ├── frameworks ├── index.php ├── laravel.php ├── lumen.php └── think.php ├── functions ├── AdapterFunctions.php └── AdapterSessionFunctions.php └── start.php /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Joan Miquel (https://github.com/joanhey) and contributors (see https://github.com/joanhey/adapterman/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 |

5 | 6 | [![Tests Adapterman](https://github.com/joanhey/AdapterMan/actions/workflows/test.yml/badge.svg)](https://github.com/joanhey/AdapterMan/actions/workflows/test.yml) 7 | [![GitHub release](https://img.shields.io/github/release/joanhey/adapterman.svg)](https://github.com/joanhey/adapterman/releases/latest) 8 | [![GitHub](https://img.shields.io/github/license/joanhey/adapterman)](https://github.com/joanhey/AdapterMan/blob/master/LICENSE.txt) 9 | 10 | Faster and more scalable apps, also use it as Serverless. 11 | 12 | Run almost any PHP app with the async event driven [Workerman](https://github.com/walkor/workerman), without touch 1 line of code in your fw or app. 13 | 14 | If your app or fw use a Front Controller, 99% that will work. Requires minimun PHP/8.0 and Adapterman/0.7 need PHP/8.1. 15 | 16 | Actually working with: 17 | - Symfony 18 | - Laravel 19 | - CakePHP 20 | - Yii2 21 | - Slim 22 | - KumbiaPHP 23 | - ThinkPHP 24 | - Leaf 25 | - FlightPHP 26 | - ... (Your app?) 27 | 28 | Still testing with more fws and apps. 29 | Without touch a line of code. 30 | 31 | ### NEW !!! Workerman shared nothing mode 32 | We started to test it. Experimental still. 33 | 34 | Each request is independent and load the .php file, like with PHP-FPM. 35 | Using the same .php files. 36 | 37 | ![image](https://user-images.githubusercontent.com/249085/209589643-5b464656-4d44-4879-9bbc-deb49582a57b.png) 38 | 39 | 40 | Framework | JSON | 1-query | Multiple queries | Fortunes | Updates | Plaintext 41 | -- | -- | -- | -- | -- | -- | -- 42 | php php-fpm| 187,747 | 97,658 | 12,784 | 79,309 | 2,010 | 195,283 43 | php workerman | 822,930 | 134,475 | 15,648 | 124,923 | 4,683 | 1,161,016 44 | 45 | 46 | 47 | ## Performance bench Worker mode 48 | Results from **Techempower benchmark. 49 | Without touch a line of code.** 50 | 51 | Follow https://twitter.com/adaptermanphp for more updates. 52 | 53 | 54 | ### Symfony 6 55 | With full ORM 56 | ![image](https://user-images.githubusercontent.com/249085/209320777-13d1cc25-f350-43a4-ba2f-4db0a92c7b7a.png) 57 | Latency 58 | ![image](https://user-images.githubusercontent.com/249085/209321052-6dab2d0e-c630-48d8-a25c-5f1906f08b8f.png) 59 | 60 | 61 | Fw | Plaintext | Json | Single query | Multiple query | Updates | Fortunes 62 | -- | --| -- | -- | -- | -- | -- 63 | Symfony | 38,231 | 37,557 | 12,578 | 10,741 | 3,420 | 10,741 64 | **Symfony Workerman** | **210,796** | **197,059** | **107,050** | **13,401** | **4,062** | **71,092** 65 | 66 | ### Laravel 8 67 | With full ORM. 68 | 69 | Fw | Plaintext | Json | Single query | Multiple query | Updates | Fortunes 70 | -- | --| -- | -- | -- | -- | -- 71 | Laravel | 14,799 | 14,770 | 9,263 | 3,247 | 1,452 | 8,354 72 | Laravel Roadrunner | 482 | 478 | 474 | 375 | 359 | 472 73 | Laravel Swoole | 38,824 | 37,439 | 21,687 | 3,958 | 1,588 | 16,035 74 | Laravel Laravel s | 54,617 | 49,372 | 23,677 | 2,917 | 1,255 | 16,696 75 | **Laravel Workerman** | **103,004** | **99,891** | **46,001** | **5,828** | **1,666** | **27,158** 76 | 77 | ![image](https://user-images.githubusercontent.com/249085/200189417-06fa658b-92c3-4c6d-a6e4-1efb3446a513.png) 78 | Latency 79 | ![image](https://user-images.githubusercontent.com/249085/200189427-99977bb7-5910-4d17-a47c-7242e8f95f8f.png) 80 | 81 | 82 | 83 | ### Slim with Workerman 84 | Without ORM 85 | ![image](https://user-images.githubusercontent.com/249085/201919385-ad25e41b-9887-42b7-92c0-d524a5e6aeae.png) 86 | 87 | Framework | Plaintext | JSON | 1-query | 20-query | Updates | Fortunes 88 | -- | -- | -- | -- | -- | -- | -- 89 | Slim 4 | 35,251 | 38,305 | 34,272 | 12,579 | 2,097 | 32,634 90 | **Slim 4 Workerman** | **134,531** | **129,393** | **81,889** | **15,803** | **2,456** | **73,212** 91 | Slim 4 Workerman pgsql * | | | 102,926 | 19,637 | 14,875 | 92,752 92 | 93 | * Without ORM and db class optimized for Workerman 94 | 95 | ### Symfony demo with Workerman 96 | Symfony initialization 0ms and half the time per request. 97 | 98 | https://user-images.githubusercontent.com/249085/197399760-5da8311e-5cf1-426a-a89d-ec2a2de43af0.mp4 99 | 100 | ## Installation 101 | ``` 102 | composer require joanhey/adapterman 103 | ``` 104 | Automatically install Workerman too. 105 | 106 | ## Tree 107 | Where to create the files (`server.php` and `start.php`) 108 | 109 | ``` 110 | . 111 | ├── app(dir) 112 | ├── public(dir) 113 | ├── vendor(dir) 114 | ├── composer.json 115 | ├── server.php 116 | └── start.php 117 | ``` 118 | 119 | ## Server 120 | server.php 121 | ```php 122 | count = 8; 133 | $http_worker->name = 'AdapterMan'; 134 | 135 | $http_worker->onWorkerStart = static function () { 136 | //init(); 137 | require __DIR__.'/start.php'; 138 | }; 139 | 140 | $http_worker->onMessage = static function ($connection, $request) { 141 | 142 | $connection->send(run()); 143 | }; 144 | 145 | Worker::runAll(); 146 | 147 | ``` 148 | ## Front Controller 149 | 150 | It's different for any fw and app. 151 | 152 | We are creating recipes for popular apps and frameworks. 153 | 154 | - [Symfony](recipes/symfony.md) 155 | - [Laravel](recipes/laravel.md) 156 | - [Lumen](recipes/lumen.md) 157 | - [Slim](recipes/slim.md) 158 | 159 | Recommended `start.php` and leave `index.php` in public. 160 | 161 | We can run the app with Workerman and with php-fpm at the same time. 162 | 163 | 164 | ## Available commands in workerman 165 | To run your app. 166 | 167 | ```php server.php start ``` 168 | ```php server.php start -d ``` 169 | ```php server.php status ``` 170 | ```php server.php status -d ``` 171 | ```php server.php connections``` 172 | ```php server.php stop ``` 173 | ```php server.php stop -g ``` 174 | ```php server.php restart ``` 175 | ```php server.php reload ``` 176 | ```php server.php reload -g ``` 177 | 178 | Workerman documentation: 179 | [https://github.com/walkor/workerman-manual](https://github.com/walkor/workerman-manual/blob/master/english/SUMMARY.md) 180 | 181 | You can also select a diferent cli php.ini directly: 182 | 183 | ```php -c cli-php.ini server.php start``` 184 | 185 | ### Help with session issues 186 | I was using this lib internally, for more than 2 years, to run legacy apps with Workerman. 187 | 188 | We made it for APIs and microservices. So the session is not well tested. 189 | 190 | #### Login progress 191 | It's working with Symfony and Laravel 192 | 193 | Laravel Orchid admin panel. 194 | ![image](https://user-images.githubusercontent.com/249085/197333441-74fff586-b984-492f-8cd1-58fb69774b1f.png) 195 | 196 | Drupal showing public pages. 197 | ![image](https://user-images.githubusercontent.com/249085/197333512-0f840436-399f-4000-b9af-e6a05a7d30b2.png) 198 | 199 | -------------------------------------------------------------------------------- /adapterman: -------------------------------------------------------------------------------- 1 | /usr/bin/env php -c vendor/joanhey/adapterman/cli-php.ini vendor/joanhey/adapterman/src/start.php "$@" 2 | -------------------------------------------------------------------------------- /cli-php.ini: -------------------------------------------------------------------------------- 1 | opcache.enable=1 2 | opcache.enable_cli=1 3 | opcache.validate_timestamps=0 4 | opcache.save_comments=0 5 | opcache.enable_file_override=1 6 | opcache.huge_code_pages=1 7 | 8 | memory_limit = 512M 9 | 10 | opcache.jit_buffer_size = 128M 11 | opcache.jit = tracing 12 | 13 | disable_functions=header,header_remove,headers_sent,headers_list,http_response_code,setcookie,session_create_id,session_id,session_name,session_save_path,session_status,session_start,session_write_close,session_regenerate_id,session_unset,session_get_cookie_params,session_set_cookie_params,set_time_limit 14 | ;disable_classes= 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joanhey/adapterman", 3 | "type": "library", 4 | "keywords": [ 5 | "workerman", 6 | "php", 7 | "event" 8 | ], 9 | "license": "MIT", 10 | "description": "Use any framework and application with Workerman.", 11 | "authors": [ 12 | { 13 | "name": "Joanhey", 14 | "email": "joanhey@kumbiaphp.com", 15 | "homepage": "https://kumbiaphp.com", 16 | "role": "Developer" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/joanhey/adapterman/issues", 21 | "source": "https://github.com/joanhey/adapterman" 22 | }, 23 | "require": { 24 | "php": "^8.1", 25 | "workerman/workerman": "^4.1|^5.1" 26 | }, 27 | "require-dev": { 28 | "ext-curl": "*", 29 | "pestphp/pest": "^2.8", 30 | "mockery/mockery": "^1.6", 31 | "guzzlehttp/guzzle": "^7.0" 32 | }, 33 | "bin": ["adapterman"], 34 | "suggest": { 35 | "ext-event": "For better performance. " 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "Adapterman\\": "src/" 40 | } 41 | }, 42 | "autoload-dev": { 43 | "psr-4": { 44 | "Tests\\": "tests/" 45 | } 46 | }, 47 | "config": { 48 | "allow-plugins": { 49 | "pestphp/pest-plugin": true 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /recipes/README.md: -------------------------------------------------------------------------------- 1 | 2 | Nginx config example [nginx-config.md](nginx-config.md) 3 | 4 | Caddy config example [caddy-config.md](caddy-config.md). 5 | 6 | Adapterman is for now an App Server. 7 | 8 | 9 | Remember, than you don't need the Composer Autoload. 10 | Because the Workerman server, include the Composer Autoload. 11 | 12 | ### Help us to create more recipes 13 | 14 | Add your recipe!! 15 | -------------------------------------------------------------------------------- /recipes/bolt.md: -------------------------------------------------------------------------------- 1 | # Bolt with Workerman 2 | 3 | Copy your `app/index.php` to `start.php`. 4 | 5 | ## Change the code 6 | In `start.php` 7 | 8 | Change: 9 | ```php 10 | bootEnv(dirname(__DIR__) . '/.env'); 29 | 30 | if ($_SERVER['APP_DEBUG']) { 31 | umask(0000); 32 | 33 | Debug::enable(); 34 | } 35 | 36 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 37 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 38 | } 39 | 40 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 41 | Request::setTrustedHosts([$trustedHosts]); 42 | } 43 | 44 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 45 | $request = Request::createFromGlobals(); 46 | $response = $kernel->handle($request); 47 | $response->send(); 48 | $kernel->terminate($request, $response); 49 | ``` 50 | 51 | To: 52 | ```php 53 | bootEnv(dirname(__DIR__) . '/.env'); 72 | 73 | if ($_SERVER['APP_DEBUG']) { 74 | umask(0000); 75 | 76 | Debug::enable(); 77 | } 78 | 79 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 80 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 81 | } 82 | 83 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 84 | Request::setTrustedHosts([$trustedHosts]); 85 | } 86 | 87 | global $kernel; 88 | 89 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 90 | 91 | function run(): string 92 | { 93 | global $kernel; 94 | 95 | ob_start(); 96 | 97 | $request = Request::createFromGlobals(); 98 | $response = $kernel->handle($request); 99 | $response->send(); 100 | $kernel->terminate($request, $response); 101 | 102 | return ob_get_clean(); 103 | } 104 | 105 | ``` 106 | 107 | ## Run your app 108 | ```php server.php start ``` 109 | 110 | 111 | View in your browser 112 | 113 | ```http://localhost:8080``` 114 | -------------------------------------------------------------------------------- /recipes/caddy-config.md: -------------------------------------------------------------------------------- 1 | Example of [Caddy](https://caddyserver.com/) configuration 2 | 3 | Caddyfile 4 | ``` 5 | your.domain.com { 6 | encode zstd gzip 7 | root * /path/to/public 8 | file_server 9 | reverse_proxy localhost:8080 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /recipes/drupal.md: -------------------------------------------------------------------------------- 1 | # Drupal with Workerman 2 | 3 | Copy your `app/index.php` to `start.php`. 4 | 5 | ## Change the code 6 | In `start.php` 7 | 8 | Change: 9 | ```php 10 | handle($request); 29 | $response->send(); 30 | 31 | $kernel->terminate($request, $response); 32 | ``` 33 | 34 | To: 35 | ```php 36 | handle($request); 63 | $response->send(); 64 | $kernel->terminate($request, $response); 65 | 66 | return ob_get_clean(); 67 | } 68 | 69 | ``` 70 | 71 | ## Run your app 72 | ```php server.php start ``` 73 | 74 | 75 | View in your browser 76 | 77 | ```http://localhost:8080``` 78 | -------------------------------------------------------------------------------- /recipes/laravel.md: -------------------------------------------------------------------------------- 1 | # Laravel with Adapterman 2 | 3 | ## server.php 4 | 5 | Create `server.php` in the project root directory with next content: 6 | ```php 7 | count = cpu_count(); // or any positive integer 18 | $http_worker->name = env('APP_NAME'); // or any string 19 | 20 | $http_worker->onWorkerStart = static function () { 21 | require __DIR__.'/start.php'; 22 | }; 23 | 24 | $http_worker->onMessage = static function ($connection, $request) { 25 | $connection->send(run()); 26 | }; 27 | 28 | Worker::runAll(); 29 | ``` 30 | 31 | ## start.php 32 | 33 | Copy your `./public/index.php` to `./start.php`. 34 | 35 | ### Change the code 36 | 37 | In the newly created `start.php` 38 | 39 | Replace this part: 40 | ```php 41 | make(Illuminate\Contracts\Http\Kernel::class); 57 | 58 | $response = $kernel->handle( 59 | $request = Illuminate\Http\Request::capture() 60 | ); 61 | 62 | $response->send(); 63 | 64 | $kernel->terminate($request, $response); 65 | 66 | ``` 67 | 68 | With this part: 69 | ```php 70 | make(Illuminate\Contracts\Http\Kernel::class); 88 | 89 | 90 | function run() 91 | { 92 | global $kernel; 93 | 94 | ob_start(); 95 | 96 | $response = $kernel->handle( 97 | $request = Illuminate\Http\Request::capture() 98 | ); 99 | 100 | $response->send(); 101 | 102 | $kernel->terminate($request, $response); 103 | 104 | return ob_get_clean(); 105 | } 106 | 107 | 108 | ``` 109 | 110 | ## Run your app 111 | 112 | In the project root directory run: 113 | 114 | ```shell 115 | php server.php start 116 | ``` 117 | 118 | View in your browser 119 | 120 | ```http://localhost:8080``` 121 | -------------------------------------------------------------------------------- /recipes/lumen.md: -------------------------------------------------------------------------------- 1 | The Front Controller for Lumen Framework. 2 | 3 | start.php 4 | ```php 5 | run(); 40 | 41 | return ob_get_clean(); 42 | } 43 | 44 | ``` 45 | -------------------------------------------------------------------------------- /recipes/nginx-config.md: -------------------------------------------------------------------------------- 1 | Example Nginx config 2 | 3 | Nginx can do: 4 | * the TLS termination 5 | * serve static files 6 | * proxy all your workerman apps in the same domain 7 | * .... 8 | 9 | nginx.conf 10 | ```nginx 11 | server { 12 | listen 80 default_server; 13 | listen [::]:80 default_server ipv6only=on; 14 | 15 | # Change to your public dir 16 | root /var/www/html/your-app/public; 17 | index index.html index.htm; 18 | 19 | server_name localhost; 20 | 21 | location / { 22 | try_files $uri $uri/ @backend; 23 | } 24 | 25 | # Add the ip:port of your app 26 | location @backend { 27 | proxy_pass 127.0.0.1:8080; // or localhost:8080; 28 | proxy_http_version 1.1; 29 | proxy_set_header Connection ""; 30 | } 31 | 32 | location ~ /\. { 33 | deny all; 34 | } 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /recipes/slim.md: -------------------------------------------------------------------------------- 1 | # Slim with Workerman 2 | 3 | Copy your `app/index.php` to `start.php`. 4 | 5 | ## Change the code 6 | In `start.php` 7 | 8 | Change: 9 | ```php 10 | $app->run(); 11 | ``` 12 | 13 | To: 14 | ```php 15 | function run(): string 16 | { 17 | global $app; 18 | ob_start(); 19 | 20 | $app->run(); 21 | 22 | return ob_get_clean(); 23 | } 24 | 25 | 26 | ``` 27 | And add `global $app;` before create the `$app` variable. 28 | 29 | ```php 30 | use Psr\Http\Message\ResponseInterface as Response; 31 | use Psr\Http\Message\ServerRequestInterface as Request; 32 | use Slim\Factory\AppFactory; 33 | 34 | global $app; // workerman 35 | 36 | AppFactory::setContainer(new \DI\Container()); 37 | $app = AppFactory::create(); 38 | 39 | ``` 40 | 41 | ## Run your app 42 | ```php server.php start ``` 43 | 44 | 45 | View in your browser 46 | 47 | ```http://localhost:8080``` -------------------------------------------------------------------------------- /recipes/symfony.md: -------------------------------------------------------------------------------- 1 | # Symfony with Workerman 2 | 3 | Copy your `app/index.php` to `start.php`. 4 | 5 | ## Change the code 6 | In `start.php` 7 | 8 | Change: 9 | ```php 10 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 11 | $request = Request::createFromGlobals(); 12 | $response = $kernel->handle($request); 13 | $response->send(); 14 | $kernel->terminate($request, $response); 15 | ``` 16 | 17 | To: 18 | ```php 19 | global $kernel; 20 | 21 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 22 | 23 | function run() 24 | { 25 | global $kernel; 26 | 27 | ob_start(); 28 | 29 | $request = Request::createFromGlobals(); 30 | $response = $kernel->handle($request); 31 | $response->send(); 32 | $kernel->terminate($request, $response); 33 | 34 | return ob_get_clean(); 35 | } 36 | 37 | ``` 38 | 39 | ## Run your app 40 | ```php server.php start ``` 41 | 42 | 43 | View in your browser 44 | 45 | ```http://localhost:8080``` 46 | -------------------------------------------------------------------------------- /recipes/thinkphp-cn.md: -------------------------------------------------------------------------------- 1 | # Thinkphp 使用 Adapterman 2 | 3 | [English](./thinkphp.md) | 中文 4 | 5 | ```shell 6 | # 安装 adapterman 到你的项目 7 | composer require joanhey/adapterman 8 | # 启动 9 | ./vendor/bin/adapterman start 10 | ``` 11 | 运行逻辑解释: 12 | 1.`./vendor/bin/adapterman start` 命令其实是执行了 13 | 14 | /usr/bin/env php -c vendor/joanhey/adapterman/cli-php.ini vendor/joanhey/adapterman/src/start.php "$@" 15 | 16 | 2.php -c [vendor/joanhey/adapterman/cli-php.ini](https://github.com/joanhey/AdapterMan/blob/master/cli-php.ini) 将一些php内置函数禁用之后,adapterman框架实现这些禁用的函数,实现在fpm下的功能在 php cli 下正常运行 17 | 18 | 3.[vendor/joanhey/adapterman/src/start.php](https://github.com/joanhey/AdapterMan/blob/master/src/start.php) 文件将自动启动服务器,并自动检测正在使用的框架. 19 | 其中 [vendor/joanhey/adapterman/src/frameworks/index.php](https://github.com/joanhey/AdapterMan/blob/master/src/frameworks/index.php) 检测正在使用的框架 20 | 21 | 在浏览器访问 22 | 23 | ```http://localhost:8080``` 24 | 25 | 26 | 或者使用 -------------------------------------------------------------------------------- /recipes/thinkphp.md: -------------------------------------------------------------------------------- 1 | # Thinkphp with Adapterman 2 | 3 | English | [中文](./thinkphp-cn.md) 4 | 5 | ```shell 6 | # install adapterman into your project 7 | composer require joanhey/adapterman 8 | # start 9 | ./vendor/bin/adapterman start 10 | ``` 11 | explain: 12 | 1.`./vendor/bin/adapterman start` Actually carried out 13 | 14 | /usr/bin/env php -c vendor/joanhey/adapterman/cli-php.ini vendor/joanhey/adapterman/src/start.php "$@" 15 | 16 | 2.php -c [vendor/joanhey/adapterman/cli-php.ini](https://github.com/joanhey/AdapterMan/blob/master/cli-php.ini) After disabling some of the built-in php functions, the adapterman framework implements these disabled functions, so that the functions under fpm will work properly under the php cli 17 | 18 | 3.[vendor/joanhey/adapterman/src/start.php](https://github.com/joanhey/AdapterMan/blob/master/src/start.php) The file will automatically start the server and automatically detect which framework is being used. 19 | among [vendor/joanhey/adapterman/src/frameworks/index.php](https://github.com/joanhey/AdapterMan/blob/master/src/frameworks/index.php) Detect the framework being used 20 | View in your browser 21 | 22 | ```http://localhost:8080``` 23 | -------------------------------------------------------------------------------- /src/Adapterman.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Adapterman; 16 | 17 | use Workerman\Worker; 18 | use Exception; 19 | 20 | class Adapterman 21 | { 22 | public const VERSION = "0.7.1"; 23 | 24 | public const NAME = 'Adapterman/'. self::VERSION. ' (Workerman/'. Worker::VERSION. ')'; 25 | 26 | private const FUNCTIONS = [ 27 | 'header', 28 | 'header_remove', 29 | 'headers_sent', 30 | 'headers_list', 31 | 'http_response_code', 32 | 33 | 'setcookie', 34 | 35 | 'session_create_id', 36 | 'session_id', 37 | 'session_name', 38 | 'session_save_path', 39 | 'session_status', 40 | 'session_start', 41 | 'session_write_close', 42 | 'session_regenerate_id', 43 | 'session_unset', 44 | 'session_get_cookie_params', 45 | 'session_set_cookie_params', 46 | 47 | 'set_time_limit', 48 | ]; 49 | 50 | public static function init(): void 51 | { 52 | try { 53 | self::checkVersion(); 54 | self::checkFunctionsDisabled(); 55 | 56 | // OK initialize the functions 57 | require __DIR__ . '/functions/AdapterFunctions.php'; 58 | require __DIR__ . '/functions/AdapterSessionFunctions.php'; 59 | class_alias(Http::class, \Protocols\Http::class); 60 | Http::init(); 61 | 62 | } catch (Exception $e) { 63 | fwrite(STDERR, self::NAME . ' Error:' . PHP_EOL); 64 | fwrite(STDERR, $e->getMessage()); 65 | exit; 66 | } 67 | 68 | fwrite(STDOUT, self::NAME . ' OK' . PHP_EOL); 69 | } 70 | 71 | /** 72 | * Check PHP version 73 | * 74 | * @throws Exception 75 | * @return void 76 | */ 77 | private static function checkVersion(): void 78 | { 79 | if (\PHP_MAJOR_VERSION < 8) { 80 | throw new Exception("* PHP version must be 8 or higher." . PHP_EOL . "* Actual PHP version: " . \PHP_VERSION . PHP_EOL); 81 | } 82 | } 83 | 84 | /** 85 | * Check that functions are disabled in php.ini 86 | * 87 | * @throws Exception 88 | * @return void 89 | */ 90 | private static function checkFunctionsDisabled(): void 91 | { 92 | 93 | foreach (self::FUNCTIONS as $function) { 94 | if (\function_exists($function)) { 95 | throw new Exception("Functions not disabled in php.ini." . PHP_EOL . self::showConfiguration()); 96 | } 97 | } 98 | } 99 | 100 | private static function showConfiguration(): string 101 | { 102 | $inipath = \php_ini_loaded_file(); 103 | $methods = \implode(',', self::FUNCTIONS); 104 | 105 | return "Add in file: $inipath" . PHP_EOL . "disable_functions=$methods" . PHP_EOL; 106 | } 107 | } 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Adapterman; 16 | 17 | use Workerman\Connection\TcpConnection; 18 | //use Workerman\Timer; 19 | 20 | /** 21 | * http protocol 22 | */ 23 | class Http 24 | { 25 | use ParseMultipart, Session; 26 | 27 | /** 28 | * Http status. 29 | */ 30 | public static string $status = ''; 31 | 32 | /** 33 | * Headers. 34 | */ 35 | public static array $headers = []; 36 | 37 | /** 38 | * Cookies. 39 | */ 40 | public static array $cookies = []; 41 | 42 | /** 43 | * Cache. 44 | */ 45 | protected static array $cache = []; 46 | 47 | /** 48 | * Send content in response 49 | * to not send with HEAD request or 204 and 304 response 50 | * 51 | * @var boolean 52 | */ 53 | protected static bool $responseContent = true; 54 | 55 | /** 56 | * Phrases. 57 | * 58 | * @var array 59 | * 60 | * @link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 61 | */ 62 | const CODES = [ 63 | 100 => 'Continue', 64 | 101 => 'Switching Protocols', 65 | 102 => 'Processing', // WebDAV; RFC 2518 66 | 103 => 'Early Hints', // RFC 8297 67 | 68 | 200 => 'OK', 69 | 201 => 'Created', 70 | 202 => 'Accepted', 71 | 203 => 'Non-Authoritative Information', // since HTTP/1.1 72 | 204 => 'No Content', 73 | 205 => 'Reset Content', 74 | 206 => 'Partial Content', // RFC 7233 75 | 207 => 'Multi-Status', // WebDAV; RFC 4918 76 | 208 => 'Already Reported', // WebDAV; RFC 5842 77 | 226 => 'IM Used', // RFC 3229 78 | 79 | 300 => 'Multiple Choices', 80 | 301 => 'Moved Permanently', 81 | 302 => 'Found', // Previously "Moved temporarily" 82 | 303 => 'See Other', // since HTTP/1.1 83 | 304 => 'Not Modified', // RFC 7232 84 | 305 => 'Use Proxy', // since HTTP/1.1 85 | 306 => 'Switch Proxy', 86 | 307 => 'Temporary Redirect', // since HTTP/1.1 87 | 308 => 'Permanent Redirect', // RFC 7538 88 | 89 | 400 => 'Bad Request', 90 | 401 => 'Unauthorized', // RFC 7235 91 | 402 => 'Payment Required', 92 | 403 => 'Forbidden', 93 | 404 => 'Not Found', 94 | 405 => 'Method Not Allowed', 95 | 406 => 'Not Acceptable', 96 | 407 => 'Proxy Authentication Required', // RFC 7235 97 | 408 => 'Request Timeout', 98 | 409 => 'Conflict', 99 | 410 => 'Gone', 100 | 411 => 'Length Required', 101 | 412 => 'Precondition Failed', // RFC 7232 102 | 413 => 'Payload Too Large', // RFC 7231 103 | 414 => 'URI Too Long', // RFC 7231 104 | 415 => 'Unsupported Media Type', // RFC 7231 105 | 416 => 'Range Not Satisfiable', // RFC 7233 106 | 417 => 'Expectation Failed', 107 | 418 => 'I\'m a teapot', // RFC 2324, RFC 7168 108 | 421 => 'Misdirected Request', // RFC 7540 109 | 422 => 'Unprocessable Entity', // WebDAV; RFC 4918 110 | 423 => 'Locked', // WebDAV; RFC 4918 111 | 424 => 'Failed Dependency', // WebDAV; RFC 4918 112 | 425 => 'Too Early', // RFC 8470 113 | 426 => 'Upgrade Required', 114 | 428 => 'Precondition Required', // RFC 6585 115 | 429 => 'Too Many Requests', // RFC 6585 116 | 431 => 'Request Header Fields Too Large', // RFC 6585 117 | 451 => 'Unavailable For Legal Reasons', // RFC 7725 118 | 119 | 500 => 'Internal Server Error', 120 | 501 => 'Not Implemented', 121 | 502 => 'Bad Gateway', 122 | 503 => 'Service Unavailable', 123 | 504 => 'Gateway Timeout', 124 | 505 => 'HTTP Version Not Supported', 125 | 506 => 'Variant Also Negotiates', // RFC 2295 126 | 507 => 'Insufficient Storage', // WebDAV; RFC 4918 127 | 508 => 'Loop Detected', // WebDAV; RFC 5842 128 | 510 => 'Not Extended', // RFC 2774 129 | 511 => 'Network Authentication Required', // RFC 6585 130 | ]; 131 | 132 | public static function init(): void 133 | { 134 | static::sessionInit(); 135 | //static::uploadInit(); 136 | } 137 | 138 | /** 139 | * Reset. 140 | * 141 | */ 142 | public static function reset(): void 143 | { 144 | static::$status = 'HTTP/1.1 200 OK'; 145 | static::$headers = [ 146 | 'Content-Type' => 'Content-Type: text/html;charset=utf-8', 147 | 'Server' => 'Server: workerman', 148 | ]; 149 | static::$cookies = []; 150 | static::$sessionFile = ''; 151 | static::$sessionStarted = false; 152 | static::$responseContent = true; 153 | } 154 | 155 | /** 156 | * The supported HTTP methods 157 | * 158 | * @var string[] 159 | */ 160 | const AVAILABLE_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS']; 161 | 162 | /** 163 | * Send a raw HTTP header. 164 | */ 165 | public static function header(string $content, bool $replace = true, int $http_response_code = 0): void 166 | { 167 | if (\str_starts_with($content, 'HTTP')) { 168 | static::$status = $content; 169 | 170 | return; 171 | } 172 | 173 | $key = \strstr($content, ':', true); 174 | if (empty($key)) { 175 | return; 176 | } 177 | 178 | if ('location' === \strtolower($key)) { 179 | if ($http_response_code === 0) { 180 | $http_response_code = 302; 181 | } 182 | static::responseCode($http_response_code); 183 | } 184 | 185 | if ($key === 'Set-Cookie') { 186 | static::$cookies[] = $content; 187 | } else { 188 | static::$headers[$key] = $content; 189 | } 190 | } 191 | 192 | /** 193 | * Remove previously set headers. 194 | * 195 | */ 196 | public static function headerRemove(?string $name = null): void 197 | { 198 | if ($name === null) { 199 | static::$headers = []; 200 | static::$cookies = []; 201 | 202 | return; 203 | } 204 | 205 | unset(static::$headers[$name]); 206 | } 207 | 208 | /** 209 | * Sets the HTTP response status code. 210 | * 211 | * @param int $code The response code 212 | * @return bool|int The valid status code or FALSE if code is not provided and it is not invoked in a web server environment 213 | */ 214 | public static function responseCode(int $code): bool|int 215 | { 216 | if (isset(static::CODES[$code])) { 217 | static::$status = "HTTP/1.1 $code " . static::CODES[$code]; 218 | 219 | return $code; 220 | } 221 | 222 | return false; 223 | } 224 | 225 | /** 226 | * Set cookie. 227 | * 228 | * @see https://www.php.net/manual/en/function.setcookie 229 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie 230 | * @see https://github.com/GoogleChromeLabs/samesite-examples/blob/master/php.md 231 | */ 232 | public static function setCookie( 233 | string $name, 234 | string $value = '', 235 | int $expires = 0, 236 | string $path = '', 237 | string $domain = '', 238 | bool $secure = false, 239 | bool $httponly = false, 240 | string $samesite = '', 241 | ): bool 242 | { 243 | if (! static::checkCookieSamesite($samesite)) { 244 | $samesite = ''; 245 | } 246 | 247 | static::$cookies[] = 'Set-Cookie: ' . $name . '=' . \rawurlencode($value) 248 | . ($domain ? '; Domain=' . $domain : '') 249 | . (($expires === 0) ? '' : '; Max-Age=' . $expires) 250 | . ($path ? '; Path=' . $path : '') 251 | . ($secure ? '; Secure' : '') 252 | . ($httponly ? '; HttpOnly' : '') 253 | . ($samesite ? "; SameSite=$samesite" : ''); 254 | 255 | return true; 256 | } 257 | 258 | // TODO: add setrawcookie 259 | 260 | protected static function checkCookieSamesite(string $samesite): bool 261 | { 262 | return \in_array($samesite, ['None', 'Lax', 'Strict']); 263 | } 264 | 265 | /** 266 | * Returns a list of response headers sent (or ready to send) 267 | * 268 | * @return array 269 | */ 270 | public static function headers_list(): array 271 | { 272 | return [...static::$cookies, ...static::$headers]; 273 | } 274 | 275 | /** 276 | * Check the integrity of the package. 277 | */ 278 | public static function input(string $recv_buffer, TcpConnection $connection): int 279 | { 280 | if (isset(static::$cache[$recv_buffer]['input'])) { 281 | return static::$cache[$recv_buffer]['input']; 282 | } 283 | $recv_len = \strlen($recv_buffer); 284 | $crlf_post = \strpos($recv_buffer, "\r\n\r\n"); 285 | if (!$crlf_post) { 286 | // Judge whether the package length exceeds the limit. 287 | if ($recv_len >= $connection->maxPackageSize) { 288 | $connection->close(); 289 | } 290 | 291 | return 0; 292 | } 293 | $head_len = $crlf_post + 4; 294 | 295 | $method = \substr($recv_buffer, 0, \strpos($recv_buffer, ' ')); 296 | if (!\in_array($method, static::AVAILABLE_METHODS)) { 297 | $connection->send("HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\n\r\n", true); 298 | $connection->consumeRecvBuffer($recv_len); 299 | 300 | return 0; 301 | } 302 | 303 | if ($method === 'GET' || $method === 'OPTIONS' || $method === 'HEAD') { 304 | static::$cache[$recv_buffer]['input'] = $head_len; 305 | if ($method === 'HEAD') { 306 | static::$responseContent = false; 307 | } 308 | 309 | return $head_len; 310 | } 311 | 312 | $match = []; 313 | if (\preg_match("/\r\nContent-Length: ?(\d+)/i", $recv_buffer, $match)) { 314 | $content_length = $match[1] ?? 0; 315 | $total_length = (int) $content_length + $head_len; 316 | if (!isset($recv_buffer[1024])) { 317 | static::$cache[$recv_buffer]['input'] = $total_length; 318 | } 319 | 320 | return $total_length; 321 | } 322 | 323 | return ($method === 'DELETE' || $method === 'PATCH') ? $head_len : 0; 324 | } 325 | 326 | /** 327 | * Parse $_POST、$_GET、$_COOKIE. 328 | */ 329 | public static function decode(string $recv_buffer, TcpConnection $connection): void 330 | { 331 | static::reset(); 332 | if (isset(static::$cache[$recv_buffer]['decode'])) { 333 | $cache = static::$cache[$recv_buffer]['decode']; 334 | $_SERVER = $cache['server']; 335 | $_POST = $cache['post']; 336 | $_GET = $cache['get']; 337 | $_COOKIE = $cache['cookie']; 338 | $_REQUEST = $cache['request']; 339 | $GLOBALS['HTTP_RAW_POST_DATA'] = $GLOBALS['HTTP_RAW_REQUEST_DATA'] = ''; 340 | 341 | return; 342 | } 343 | // Init. 344 | $_POST = $_GET = $_COOKIE = $_REQUEST = $_SESSION = $_FILES = []; 345 | // $_SERVER 346 | $_SERVER = [ 347 | 'REQUEST_METHOD' => '', 348 | 'REQUEST_URI' => '', 349 | 'SERVER_PROTOCOL' => '', 350 | 'SERVER_ADDR' => $connection->getLocalIp(), 351 | 'SERVER_PORT' => $connection->getLocalPort(), 352 | 'REMOTE_ADDR' => $connection->getRemoteIp(), 353 | 'REMOTE_PORT' => $connection->getRemotePort(), 354 | 'SERVER_SOFTWARE' => Adapterman::NAME, 355 | 'SERVER_NAME' => '', 356 | 'HTTP_HOST' => '', 357 | 'HTTP_USER_AGENT' => '', 358 | 'HTTP_ACCEPT' => '', 359 | 'HTTP_ACCEPT_LANGUAGE' => '', 360 | 'HTTP_ACCEPT_ENCODING' => '', 361 | 'HTTP_COOKIE' => '', 362 | 'HTTP_CONNECTION' => '', 363 | 'CONTENT_TYPE' => '', 364 | ]; 365 | 366 | // Parse headers. 367 | [$http_header, $http_body] = \explode("\r\n\r\n", $recv_buffer, 2); 368 | $header_data = \explode("\r\n", $http_header); 369 | 370 | [$_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'], $_SERVER['SERVER_PROTOCOL']] = \explode(' ', $header_data[0]); 371 | 372 | $http_post_boundary = ''; 373 | unset($header_data[0]); 374 | foreach ($header_data as $content) { 375 | // \r\n\r\n 376 | if (empty($content)) { 377 | continue; 378 | } 379 | [$key, $value] = \explode(':', $content, 2); 380 | $key = \str_replace('-', '_', \strtoupper($key)); 381 | $value = \trim($value); 382 | $_SERVER['HTTP_' . $key] = $value; 383 | switch ($key) { 384 | // HTTP_HOST 385 | case 'HOST': 386 | $tmp = \explode(':', $value); 387 | $_SERVER['SERVER_NAME'] = $tmp[0]; 388 | $_SERVER['SERVER_PORT'] = (int) ($tmp[1] ?? 80); 389 | break; 390 | // cookie 391 | case 'COOKIE': 392 | \parse_str(\str_replace('; ', '&', $_SERVER['HTTP_COOKIE']), $_COOKIE); 393 | break; 394 | // content-type 395 | case 'CONTENT_TYPE': 396 | if (!\preg_match('/boundary="?(\S+)"?/', $value, $match)) { 397 | if ($pos = \strpos($value, ';')) { 398 | $_SERVER['CONTENT_TYPE'] = \substr($value, 0, $pos); 399 | } else { 400 | $_SERVER['CONTENT_TYPE'] = $value; 401 | } 402 | } else { 403 | $_SERVER['CONTENT_TYPE'] = 'multipart/form-data'; 404 | $http_post_boundary = '--' . $match[1]; 405 | } 406 | break; 407 | case 'CONTENT_LENGTH': 408 | $_SERVER['CONTENT_LENGTH'] = $value; 409 | break; 410 | } 411 | } 412 | 413 | // Parse $_POST. 414 | if ($_SERVER['REQUEST_METHOD'] === 'POST' && $_SERVER['CONTENT_TYPE']) { 415 | match ($_SERVER['CONTENT_TYPE']) { 416 | 'multipart/form-data' => static::parseMultipart($http_body, $http_post_boundary), 417 | 'application/json' => $_POST = \json_decode($http_body, true, flags: \JSON_THROW_ON_ERROR) ?? [], 418 | 'application/x-www-form-urlencoded' => \parse_str($http_body, $_POST), 419 | default => '' 420 | }; 421 | } 422 | 423 | // Parse other HTTP action parameters 424 | if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'POST') { 425 | $data = []; 426 | match ($_SERVER['CONTENT_TYPE']) { 427 | 'application/x-www-form-urlencoded' => \parse_str($http_body, $data), 428 | 'application/json' => $data = \json_decode($http_body, true, flags: \JSON_THROW_ON_ERROR) ?? [], 429 | default => '' 430 | }; 431 | $_REQUEST = $data; 432 | } 433 | 434 | // HTTP_RAW_REQUEST_DATA HTTP_RAW_POST_DATA 435 | $GLOBALS['HTTP_RAW_REQUEST_DATA'] = $GLOBALS['HTTP_RAW_POST_DATA'] = $http_body; 436 | 437 | // QUERY_STRING 438 | $_SERVER['QUERY_STRING'] = \parse_url($_SERVER['REQUEST_URI'], \PHP_URL_QUERY); 439 | if ($_SERVER['QUERY_STRING']) { 440 | // $GET 441 | \parse_str($_SERVER['QUERY_STRING'], $_GET); 442 | } else { 443 | $_SERVER['QUERY_STRING'] = ''; 444 | } 445 | 446 | // REQUEST 447 | $_REQUEST = [...$_GET, ...$_POST, ...$_REQUEST]; 448 | 449 | if ($_SERVER['REQUEST_METHOD'] === 'GET') { 450 | static::$cache[$recv_buffer]['decode'] = [ 451 | 'get' => $_GET, 452 | 'post' => $_POST, 453 | 'cookie' => $_COOKIE, 454 | 'server' => $_SERVER, 455 | 'files' => $_FILES, 456 | 'request'=> $_REQUEST, 457 | ]; 458 | if (\count(static::$cache) > 256) { 459 | unset(static::$cache[\key(static::$cache)]); 460 | } 461 | } 462 | } 463 | 464 | /** 465 | * Http encode. 466 | * 467 | * @param string $content 468 | */ 469 | public static function encode(string $content, TcpConnection $connection): string 470 | { 471 | // http-code status line. 472 | $header = static::$status . "\r\n"; 473 | 474 | // create headers 475 | if ($headers = self::headers_list()) { 476 | $header .= \implode("\r\n", $headers) . "\r\n"; 477 | } 478 | 479 | if (!empty($connection->gzip)) { 480 | $header .= "Content-Encoding: gzip\r\n"; 481 | $content = \gzencode($content, $connection->gzip); 482 | } 483 | // header 484 | $header .= 'Content-Length: ' . \strlen($content) . "\r\n\r\n"; 485 | 486 | if(!static::$responseContent) { 487 | $content = ""; 488 | } 489 | 490 | // save session 491 | static::sessionWriteClose(); 492 | 493 | // the whole http package 494 | return $header . $content; 495 | } 496 | } 497 | -------------------------------------------------------------------------------- /src/ParseMultipart.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Adapterman; 16 | 17 | trait ParseMultipart 18 | { 19 | /** 20 | * Parse multipart form data to $_POST & $_FILES. 21 | * 22 | */ 23 | protected static function parseMultipart(string $http_body, string $http_post_boundary): void 24 | { 25 | $http_body = \substr($http_body, 0, \strlen($http_body) - (\strlen($http_post_boundary) + 4)); 26 | $boundary_data_array = \explode($http_post_boundary . "\r\n", $http_body); 27 | if ($boundary_data_array[0] === '') { 28 | unset($boundary_data_array[0]); 29 | } 30 | 31 | $post_encode_string = ''; 32 | foreach ($boundary_data_array as $boundary_data_buffer) { 33 | [$boundary_header_buffer, $boundary_value] = \explode("\r\n\r\n", $boundary_data_buffer, 2); 34 | // Remove \r\n from the end of buffer. 35 | $boundary_value = \substr($boundary_value, 0, -2); 36 | 37 | // Is post field 38 | if (!strpos($boundary_header_buffer, '"; filename="')) { 39 | // Parse $_POST. 40 | $item = \explode("\r\n", $boundary_header_buffer)[0]; 41 | $header_value = \explode(': ', $item)[1]; 42 | if (\preg_match('/name="(.*?)"$/', $header_value, $match)) { 43 | $post_encode_string .= urlencode($match[1]) . '=' . urlencode($boundary_value) . '&'; 44 | } 45 | continue; 46 | }; 47 | 48 | // Is file data 49 | if (\preg_match('/name="(.*?)"/', $boundary_header_buffer, $named)) { 50 | $name = $named[1]; 51 | } else { // Unknow 52 | continue; 53 | } 54 | 55 | foreach (\explode("\r\n", $boundary_header_buffer) as $item) { 56 | [$key, $value] = \explode(': ', $item); 57 | switch (strtolower($key)) { 58 | case 'content-disposition': 59 | if (\preg_match('/"; filename="(.*?)"/', $value, $match)) { 60 | // Parse $_FILES. 61 | $_FILES[$name] = [ 62 | 'name' => $match[1], 63 | 'full_path' => $match[1], 64 | 'size' => \strlen($boundary_value), 65 | 'tmp_name' => static::saveTempFile($boundary_value), 66 | 'error' => \UPLOAD_ERR_OK, // test 67 | ]; 68 | break; 69 | } 70 | 71 | case 'content-type': 72 | // add file_type 73 | $_FILES[$name]['type'] = \trim($value); 74 | break; 75 | 76 | case 'webkitrelativepath': 77 | $_FILES[$name]['full_path'] = \trim($value); 78 | break; 79 | 80 | case 'Content-Lenght': 81 | } 82 | } 83 | } 84 | // $_POST data 85 | if ($post_encode_string) { 86 | \parse_str($post_encode_string, $_POST); 87 | } 88 | } 89 | 90 | protected static function saveTempFile($data): string 91 | { 92 | $tmp_file = \tempnam(\sys_get_temp_dir(), 'php'); 93 | \file_put_contents($tmp_file,$data); 94 | // delete tmp_file after send() 95 | 96 | return $tmp_file; 97 | } 98 | } 99 | 100 | -------------------------------------------------------------------------------- /src/Session.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | namespace Adapterman; 16 | 17 | use Workerman\Timer; 18 | 19 | trait Session 20 | { 21 | 22 | /** 23 | * Session Cookie params 24 | */ 25 | public static array $sessionCookie = [ 26 | 'lifetime' => 0, 27 | 'path' => '/', 28 | 'domain' => '', 29 | 'secure' => false, 30 | 'httponly' => false, 31 | 'samesite' => 'Lax', 32 | ]; 33 | 34 | /** 35 | * Session save path. 36 | */ 37 | public static string $sessionSavePath = ''; 38 | 39 | /** 40 | * Session name. 41 | */ 42 | public static string $sessionName = ''; 43 | 44 | /** 45 | * Session gc max lifetime. 46 | */ 47 | public static int $sessionGcMaxLifeTime = 1440; 48 | 49 | /** 50 | * Session gc interval. 51 | */ 52 | public static int $sessionGcInterval = 600; 53 | 54 | /** 55 | * Session started. 56 | */ 57 | protected static bool $sessionStarted = false; 58 | 59 | /** 60 | * Session file. 61 | */ 62 | protected static string $sessionFile = ''; 63 | 64 | /** 65 | * Init. 66 | * 67 | * @return void 68 | */ 69 | public static function sessionInit() 70 | { 71 | if (! static::$sessionName) { 72 | static::$sessionName = \ini_get('session.name'); 73 | } 74 | 75 | if (! static::$sessionSavePath) { 76 | $savePath = \ini_get('session.save_path'); 77 | if (\preg_match('/^\d+;(.*)$/', $savePath, $match)) { 78 | $savePath = $match[1]; 79 | } 80 | if (! $savePath || \str_starts_with($savePath, 'tcp://')) { 81 | $savePath = \sys_get_temp_dir(); 82 | } 83 | static::$sessionSavePath = $savePath; 84 | } 85 | 86 | if ($gc_max_life_time = \ini_get('session.gc_maxlifetime')) { 87 | static::$sessionGcMaxLifeTime = (int) $gc_max_life_time; 88 | } 89 | 90 | static::$sessionCookie['lifetime'] = (int) \ini_get('session.cookie_lifetime'); 91 | static::$sessionCookie['path'] = (string) \ini_get('session.cookie_path'); 92 | static::$sessionCookie['domain'] = (string) \ini_get('session.cookie_domain'); 93 | static::$sessionCookie['secure'] = (bool) \ini_get('session.cookie_secure'); 94 | static::$sessionCookie['httponly'] = (bool) \ini_get('session.cookie_httponly'); 95 | if (static::checkCookieSamesite(\ini_get('session.cookie_samesite'))) { 96 | static::$sessionCookie['samesite'] = \ini_get('session.cookie_samesite'); 97 | } 98 | 99 | } 100 | 101 | protected static function createSessionCookie(string $name, string $id): bool 102 | { 103 | return static::setcookie( 104 | $name, 105 | $id, 106 | static::$sessionCookie['lifetime'], 107 | static::$sessionCookie['path'], 108 | static::$sessionCookie['domain'], 109 | static::$sessionCookie['secure'], 110 | static::$sessionCookie['httponly'], 111 | static::$sessionCookie['samesite'] 112 | ); 113 | } 114 | 115 | /** 116 | * Returns the current session status 117 | * 118 | * @see https://www.php.net/manual/en/function.session-status.php 119 | */ 120 | public static function sessionStatus(): int 121 | { 122 | if (static::sessionStarted()) { 123 | if (static::$sessionFile) { 124 | return \PHP_SESSION_ACTIVE; 125 | } 126 | 127 | return \PHP_SESSION_NONE; 128 | } 129 | 130 | 131 | return \PHP_SESSION_DISABLED; 132 | } 133 | 134 | /** 135 | * Session create id. 136 | * @see https://www.php.net/manual/en/function.session-create-id.php 137 | */ 138 | public static function sessionCreateId(string $prefix = ''): string|false 139 | { 140 | // if ($prefix === '') { 141 | 142 | // } 143 | return \bin2hex(\pack('d', \hrtime(true)).\random_bytes(8)); 144 | } 145 | 146 | /** 147 | * Get and/or set the current session id. 148 | * 149 | * @see https://www.php.net/manual/en/function.session-id.php 150 | */ 151 | public static function sessionId(?string $id = null): string|false 152 | { 153 | if ($id === null) { 154 | if (static::sessionStarted() && static::$sessionFile) { 155 | return \str_replace('ses_', '', \basename(static::$sessionFile)); 156 | } 157 | return ''; 158 | } 159 | if (static::sessionStarted() && static::$sessionFile) { 160 | return \str_replace('ses_', '', \basename(static::$sessionFile)); 161 | } 162 | 163 | return ''; 164 | } 165 | 166 | /** 167 | * Get and/or set the current session name. 168 | * @see https://www.php.net/manual/en/function.session-name.php 169 | */ 170 | public static function sessionName(?string $name = null): string|false 171 | { 172 | if ($name === null) { 173 | return static::$sessionName; 174 | } 175 | 176 | if (! static::sessionStarted() && ! ctype_digit($name) && ctype_alnum($name)) { 177 | $session_name = static::$sessionName; 178 | static::$sessionName = $name; 179 | return $session_name; 180 | } 181 | 182 | return false; 183 | } 184 | 185 | /** 186 | * Get and/or set the current session save path. 187 | * 188 | * @see https://www.php.net/manual/en/function.session-save-path.php 189 | */ 190 | public static function sessionSavePath(?string $path = null): string|false 191 | { 192 | if ($path === null) { 193 | return static::$sessionSavePath; 194 | } 195 | 196 | if (! static::sessionStarted() && \is_dir($path) && \is_writable($path)) { 197 | return static::$sessionSavePath = $path; 198 | } 199 | 200 | return false; 201 | } 202 | 203 | /** 204 | * Session started. 205 | */ 206 | public static function sessionStarted(): bool 207 | { 208 | return static::$sessionStarted; 209 | } 210 | 211 | /** 212 | * Session start. 213 | */ 214 | public static function sessionStart(): bool 215 | { 216 | if (static::$sessionStarted) { 217 | return true; 218 | } 219 | static::$sessionStarted = true; 220 | // Check session file path 221 | if (isset($_COOKIE[static::$sessionName]) && !\preg_match('/^[a-zA-Z0-9]+$/', $_COOKIE[static::$sessionName])) { 222 | unset($_COOKIE[static::$sessionName]); 223 | } 224 | // Generate a SID. 225 | if (! isset($_COOKIE[static::$sessionName]) || ! \is_file(static::$sessionSavePath.'/ses_'.$_COOKIE[static::$sessionName])) { 226 | // Create a unique session_id and the associated file name. 227 | while (true) { 228 | $session_id = static::sessionCreateId(); 229 | if (! \is_file($file_name = static::$sessionSavePath.'/ses_'.$session_id)) { 230 | break; 231 | } 232 | } 233 | static::$sessionFile = $file_name; 234 | 235 | return static::createSessionCookie(static::$sessionName, $session_id); 236 | } 237 | 238 | if (! static::$sessionFile) { 239 | static::$sessionFile = static::$sessionSavePath.'/ses_'.$_COOKIE[static::$sessionName]; 240 | } 241 | // Read session from session file. 242 | $raw = \file_get_contents(static::$sessionFile); 243 | if ($raw) { 244 | $_SESSION = \unserialize($raw); 245 | } 246 | 247 | return true; 248 | } 249 | 250 | /** 251 | * Save session. 252 | */ 253 | public static function sessionWriteClose(): bool 254 | { 255 | if (static::$sessionStarted) { 256 | $session_str = \serialize($_SESSION); 257 | if ($session_str && static::$sessionFile) { 258 | return (bool) \file_put_contents(static::$sessionFile, $session_str); 259 | } 260 | } 261 | 262 | return empty($_SESSION); 263 | } 264 | 265 | /** 266 | * Update the current session id with a newly generated one. 267 | * 268 | * @link https://www.php.net/manual/en/function.session-regenerate-id.php 269 | */ 270 | public static function sessionRegenerateId(bool $delete_old_session = false): bool 271 | { 272 | $old_session_file = static::$sessionFile; 273 | // Create a unique session_id and the associated file name. 274 | while (true) { 275 | $session_id = static::sessionCreateId(); 276 | if (! \is_file($file_name = static::$sessionSavePath.'/ses_'.$session_id)) { 277 | break; 278 | } 279 | } 280 | static::$sessionFile = $file_name; 281 | 282 | if ($delete_old_session) { 283 | \unlink($old_session_file); 284 | } 285 | 286 | return static::createSessionCookie(static::$sessionName, $session_id); 287 | } 288 | 289 | /** 290 | * Try GC sessions. 291 | * 292 | * @return void 293 | */ 294 | public static function tryGcSessions() 295 | { 296 | $time_now = \time(); 297 | foreach (\glob(static::$sessionSavePath.'/ses*') as $file) { 298 | if (\is_file($file) && $time_now - \filemtime($file) > static::$sessionGcMaxLifeTime) { 299 | \unlink($file); 300 | } 301 | } 302 | } 303 | 304 | /** 305 | * Set the session cookie parameters 306 | * 307 | * @see https://www.php.net/manual/en/function.session-set-cookie-params.php 308 | * 309 | * @param integer $lifetime_or_options 310 | * @param string|null $path 311 | * @param string|null $domain 312 | * @param bool|null $secure 313 | * @param bool|null $httponly 314 | * 315 | * @return boolean Returns **true** on success or **false** on failure. 316 | */ 317 | public static function sessionSetCookieParams( 318 | int|array $lifetime_or_options, 319 | ?string $path = null, 320 | ?string $domain = null, 321 | ?bool $secure = null, 322 | ?bool $httponly = null 323 | ): bool 324 | { 325 | if (static::sessionStarted()) { 326 | return false; 327 | } 328 | 329 | if (\is_array($lifetime_or_options)) { 330 | //Validate keys 331 | if (\array_diff_key($lifetime_or_options, static::$sessionCookie) === []) { 332 | $options = \array_filter($lifetime_or_options, fn ($value) => !\is_null($value)); 333 | 334 | if(isset($options['samesesite']) && $options['samesesite'] && !static::checkSession($options['samesesite'])) { 335 | return false; 336 | } 337 | 338 | static::$sessionCookie = $options + static::$sessionCookie; 339 | 340 | return true; 341 | } 342 | 343 | return false; 344 | } 345 | 346 | static::$sessionCookie['lifetime'] = $lifetime_or_options; 347 | $params = [ 348 | 'path' => $path, 349 | 'domain' => $domain, 350 | 'secure' => $secure, 351 | 'httponly' => $httponly, 352 | ]; 353 | 354 | foreach ($params as $key => $value) { 355 | if (! \is_null($value)) { 356 | static::$sessionCookie[$key] = $value; 357 | } 358 | } 359 | 360 | return true; 361 | } 362 | 363 | /** 364 | * Get the session cookie parameters 365 | * 366 | * @see https://www.php.net/manual/en/function.session-get-cookie-params.php 367 | */ 368 | public static function sessionGetCookieParams(): array 369 | { 370 | return static::$sessionCookie; 371 | } 372 | } 373 | 374 | 375 | -------------------------------------------------------------------------------- /src/frameworks/index.php: -------------------------------------------------------------------------------- 1 | make(Illuminate\Contracts\Http\Kernel::class); 8 | 9 | function run() 10 | { 11 | global $kernel; 12 | 13 | ob_start(); 14 | 15 | $response = $kernel->handle( 16 | $request = Illuminate\Http\Request::capture() 17 | ); 18 | 19 | $response->send(); 20 | 21 | $kernel->terminate($request, $response); 22 | 23 | return ob_get_clean(); 24 | } 25 | -------------------------------------------------------------------------------- /src/frameworks/lumen.php: -------------------------------------------------------------------------------- 1 | run(); 12 | 13 | return ob_get_clean(); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /src/frameworks/think.php: -------------------------------------------------------------------------------- 1 | app->getBasePath() . 'middleware.php')) { 26 | // Change include to include_once OnlyOne 27 | $middleware = include_once $this->app->getBasePath() . 'middleware.php'; 28 | if (is_array($middleware)) { 29 | $this->app->middleware->import($middleware); 30 | } 31 | } 32 | } 33 | 34 | protected function loadRoutes(): void 35 | { 36 | $routePath = $this->getRoutePath(); 37 | 38 | if (is_dir($routePath)) { 39 | $files = glob($routePath . '*.php'); 40 | foreach ($files as $file) { 41 | // Change include to include_once 42 | include_once $file; 43 | } 44 | } 45 | 46 | $this->app->event->trigger(RouteLoaded::class); 47 | } 48 | } 49 | 50 | class App extends think\App 51 | { 52 | protected $bind = [ 53 | 'app' => \think\App::class, 54 | 'cache' => Cache::class, 55 | 'config' => Config::class, 56 | 'console' => Console::class, 57 | 'cookie' => Cookie::class, 58 | 'db' => Db::class, 59 | 'env' => Env::class, 60 | 'event' => Event::class, 61 | 'http' => Http::class, // Change think\Http to Http 62 | 'lang' => Lang::class, 63 | 'log' => Log::class, 64 | 'middleware' => Middleware::class, 65 | 'request' => Request::class, 66 | 'response' => Response::class, 67 | 'route' => Route::class, 68 | 'session' => Session::class, 69 | 'validate' => Validate::class, 70 | 'view' => View::class, 71 | 'think\DbManager' => Db::class, 72 | 'think\LogManager' => Log::class, 73 | 'think\CacheManager' => Cache::class, 74 | 'Psr\Log\LoggerInterface' => Log::class, 75 | ]; 76 | } 77 | 78 | function run() 79 | { 80 | static $app; 81 | ob_start(); 82 | $app = $app ?: new App(); 83 | $http = $app->http; 84 | $response = $http->run(); 85 | $response->send(); 86 | $http->end($response); 87 | return ob_get_clean(); 88 | } 89 | -------------------------------------------------------------------------------- /src/functions/AdapterFunctions.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Adapterman\Http; 16 | 17 | /** 18 | * Send a raw HTTP header 19 | * 20 | * @link https://php.net/manual/en/function.header.php 21 | */ 22 | function header(string $content, bool $replace = true, int $http_response_code = 0): void 23 | { 24 | Http::header($content, $replace, $http_response_code); 25 | } 26 | 27 | /** 28 | * Remove previously set headers 29 | * 30 | * @param string $name The header name to be removed. This parameter is case-insensitive. 31 | * @return void 32 | * 33 | * @link https://php.net/manual/en/function.header-remove.php 34 | */ 35 | function header_remove(?string $name = null): void 36 | { 37 | Http::headerRemove($name); //TODO fix case-insensitive 38 | } 39 | 40 | /** 41 | * Get or Set the HTTP response code 42 | * 43 | * @param integer $code [optional] The optional response_code will set the response code. 44 | * @return integer The current response code. By default the return value is int(200). 45 | * 46 | * @link https://www.php.net/manual/en/function.http-response-code.php 47 | */ 48 | function http_response_code(?int $code = null): int 49 | { // int|bool 50 | return Http::responseCode($code); // Fix to return actual status when void 51 | } 52 | 53 | /** 54 | * Returns a list of response headers sent (or ready to send) 55 | * 56 | * @return array 57 | * 58 | * @link https://www.php.net/manual/en/function.headers-list.php 59 | */ 60 | function headers_list(): array 61 | { 62 | return Http::headers_list(); 63 | } 64 | 65 | if (! function_exists('getallheaders')) { // It's declared in a dev lib 66 | /** 67 | * Fetch all HTTP request headers 68 | * 69 | * @return array 70 | * @link https://www.php.net/manual/en/function.getallheaders.php 71 | */ 72 | function getallheaders(): array 73 | { 74 | $headers = []; 75 | foreach ($_SERVER as $key => $value) { 76 | if (str_starts_with($key, 'HTTP_')) { 77 | $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))))] = $value; 78 | } 79 | } 80 | 81 | return $headers; 82 | } 83 | } 84 | 85 | /** 86 | * Send a cookie 87 | * 88 | * @param string $name 89 | * @param string $value 90 | * @param int|array $expires 91 | * @param string $path 92 | * @param string $domain 93 | * @param boolean $secure 94 | * @param boolean $httponly 95 | * @return boolean 96 | * 97 | * @link https://php.net/manual/en/function.setcookie.php 98 | */ 99 | function setcookie(string $name, string $value = '', int|array $expires = 0, string $path = '', string $domain = '', bool $secure = false, bool $httponly = false): bool 100 | { 101 | $samesite = ''; 102 | if (is_array($expires)) { // Alternative signature available as of PHP 7.3.0 (not supported with named parameters) 103 | $expires = $expires['expires'] ?? 0; 104 | $path = $expires['path'] ?? ''; 105 | $domain = $expires['domain'] ?? ''; 106 | $secure = $expires['secure'] ?? false; 107 | $httponly = $expires['httponly'] ?? false; 108 | $samesite = $expires['samesite'] ?? ''; 109 | } 110 | 111 | return Http::setCookie($name, $value, $expires, $path, $domain, $secure, $httponly, $samesite); 112 | } 113 | 114 | 115 | 116 | /** 117 | * Limits the maximum execution time 118 | * 119 | * @param int $seconds 120 | * @return bool 121 | */ 122 | function set_time_limit(int $seconds): bool 123 | { 124 | // Disable set_time_limit to not stop the worker 125 | // by default CLI sapi use 0 (unlimited) 126 | return true; 127 | } 128 | 129 | /** 130 | * Checks if or where headers have been sent 131 | * 132 | * @link https://www.php.net/manual/en/function.headers-sent.php 133 | * 134 | * @return bool Always false with Adapterman 135 | */ 136 | function headers_sent(?string &$filename = null, ?int &$line = null): bool 137 | { 138 | return false; 139 | } 140 | 141 | /** 142 | * Get cpu count 143 | * 144 | */ 145 | function cpu_count(): int 146 | { 147 | // Windows does not support the number of processes setting. 148 | if (\DIRECTORY_SEPARATOR === '\\') { 149 | return 1; 150 | } 151 | $count = 4; 152 | if (\is_callable('shell_exec')) { 153 | if (\strtolower(PHP_OS) === 'darwin') { 154 | $count = (int)\shell_exec('sysctl -n machdep.cpu.core_count'); 155 | } else { 156 | $count = (int)\shell_exec('nproc'); 157 | } 158 | } 159 | return $count > 0 ? $count : 2; 160 | } 161 | 162 | /* function exit(string $status = ''): void { //string|int 163 | Http::end($status); 164 | } // exit and die are language constructors, change your code with an empty ExitException 165 | */ 166 | -------------------------------------------------------------------------------- /src/functions/AdapterSessionFunctions.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright Joan Miquel 11 | * @link https://github.com/joanhey/AdapterMan 12 | * @license http://www.opensource.org/licenses/mit-license.php MIT License 13 | */ 14 | 15 | use Adapterman\Http; 16 | 17 | /** 18 | * Session functions 19 | */ 20 | 21 | /** 22 | * Create new session id 23 | * 24 | * @link https://www.php.net/manual/en/function.session-create-id.php 25 | */ 26 | function session_create_id(string $prefix = ""): string|false 27 | { 28 | return Http::sessionCreateId(); //TODO fix to use $prefix 29 | } 30 | 31 | /** 32 | * Get and/or set the current session id 33 | * 34 | * @link https://www.php.net/manual/en/function.session-id.php 35 | */ 36 | function session_id(?string $id = null): string|false 37 | { 38 | return Http::sessionId($id); //TODO fix return session name or '' if not exists session 39 | } 40 | 41 | /** 42 | * Get and/or set the current session name 43 | * 44 | * @link https://www.php.net/manual/en/function.session-name.php 45 | */ 46 | function session_name(?string $name = null): string|false 47 | { 48 | return Http::sessionName($name); 49 | } 50 | 51 | /** 52 | * Get and/or set the current session save path 53 | * 54 | * @link https://www.php.net/manual/en/function.session-save-path.php 55 | */ 56 | function session_save_path(?string $path = null): string|false 57 | { 58 | return Http::sessionSavePath($path); 59 | } 60 | 61 | /** 62 | * Returns the current session status 63 | * 64 | * @link https://www.php.net/manual/en/function.session-status.php 65 | */ 66 | function session_status(): int 67 | { 68 | return Http::sessionStatus(); 69 | } 70 | 71 | /** 72 | * Start new or resume existing session 73 | * 74 | * @link https://www.php.net/manual/en/function.session-start.php 75 | */ 76 | function session_start(array $options = []): bool 77 | { 78 | return Http::sessionStart(); //TODO fix $options 79 | } 80 | 81 | /** 82 | * Write session data and end session 83 | * 84 | * @link https://www.php.net/manual/en/function.session-write-close.php 85 | */ 86 | function session_write_close(): bool 87 | { 88 | return Http::sessionWriteClose(); 89 | } 90 | 91 | /** 92 | * Update the current session id with a newly generated one 93 | * 94 | * @link https://www.php.net/manual/en/function.session-regenerate-id.php 95 | */ 96 | function session_regenerate_id(bool $delete_old_session = false): bool 97 | { 98 | return Http::sessionRegenerateId($delete_old_session); 99 | } 100 | 101 | 102 | /** 103 | * Free all session variables 104 | * 105 | * @link https://www.php.net/manual/en/function.session-unset.php 106 | */ 107 | function session_unset(): bool 108 | { 109 | if(session_status() === PHP_SESSION_ACTIVE) { 110 | $_SESSION = []; 111 | 112 | return true; 113 | } 114 | 115 | return false; 116 | } 117 | 118 | /** 119 | * Get the session cookie parameters 120 | * 121 | * @link https://www.php.net/manual/en/function.session-get-cookie-params.php 122 | */ 123 | function session_get_cookie_params(): array 124 | { 125 | return Http::sessionGetCookieParams(); 126 | } 127 | 128 | /** 129 | * Set the session cookie parameters 130 | * 131 | * @link https://www.php.net/manual/en/function.session-set-cookie-params.php 132 | */ 133 | function session_set_cookie_params( 134 | int|array $lifetime_or_options, 135 | ?string $path = null, 136 | ?string $domain = null, 137 | ?bool $secure = null, 138 | ?bool $httponly = null 139 | ): bool 140 | { 141 | return Http::sessionSetCookieParams($lifetime_or_options, $path, $domain, $secure, $httponly); 142 | } 143 | -------------------------------------------------------------------------------- /src/start.php: -------------------------------------------------------------------------------- 1 | count = cpu_count() * 4; 15 | $http_worker->name = 'AdapterMan'; 16 | 17 | $http_worker->onWorkerStart = function (Worker $worker) { 18 | if ($worker->id === 0) { 19 | Timer::add(600, function(){ 20 | Http::tryGcSessions(); 21 | }); 22 | } 23 | }; 24 | 25 | $http_worker->onMessage = static function ($connection, $request) { 26 | $connection->send(run()); 27 | }; 28 | 29 | Worker::runAll(); 30 | --------------------------------------------------------------------------------