├── 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 | [](https://github.com/joanhey/AdapterMan/actions/workflows/test.yml)
7 | [](https://github.com/joanhey/adapterman/releases/latest)
8 | [](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 | 
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 | 
57 | Latency
58 | 
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 | 
78 | Latency
79 | 
80 |
81 |
82 |
83 | ### Slim with Workerman
84 | Without ORM
85 | 
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 | 
195 |
196 | Drupal showing public pages.
197 | 
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 |
--------------------------------------------------------------------------------