├── .editorconfig ├── .github ├── .kodiak.toml ├── ISSUE_TEMPLATE │ ├── Bug.md │ ├── Feature.md │ └── Question.md ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── conf └── build.ini ├── errors └── now-dev-no-local-php.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.ts ├── launchers │ ├── builtin.ts │ ├── cgi.ts │ ├── cli.ts │ └── helpers.ts ├── types.d.ts └── utils.ts ├── test ├── examples │ ├── 00-php │ │ ├── .gitignore │ │ ├── api │ │ │ ├── api │ │ │ │ ├── index.php │ │ │ │ └── users.php │ │ │ ├── ext │ │ │ │ ├── ds.php │ │ │ │ ├── gd.php │ │ │ │ ├── index.php │ │ │ │ └── phalcon.php │ │ │ ├── hello.php │ │ │ ├── index.php │ │ │ ├── ini │ │ │ │ └── index.php │ │ │ ├── libs.php │ │ │ └── test.php │ │ └── vercel.json │ ├── 00-test │ │ ├── .gitignore │ │ ├── api │ │ │ ├── hey.txt │ │ │ ├── index.php │ │ │ ├── php.ini │ │ │ └── test.php │ │ ├── src │ │ │ └── foo.txt │ │ └── vercel.json │ ├── 01-cowsay │ │ ├── index.php │ │ ├── subdirectory │ │ │ └── index.php │ │ └── vercel.json │ ├── 02-extensions │ │ ├── index.php │ │ └── vercel.json │ ├── 03-env-vars │ │ ├── env │ │ │ └── index.php │ │ └── vercel.json │ ├── 04-include-files │ │ ├── excluded_file.php │ │ ├── included_file.php │ │ ├── index.php │ │ └── vercel.json │ ├── 05-globals │ │ ├── index.php │ │ └── vercel.json │ ├── 06-setcookie │ │ ├── index.php │ │ └── vercel.json │ ├── 07-function │ │ ├── index.php │ │ └── vercel.json │ ├── 08-opcache │ │ ├── index.php │ │ └── vercel.json │ ├── 09-routes │ │ ├── index.php │ │ └── vercel.json │ ├── 10-composer-builds │ │ ├── .gitignore │ │ ├── .vercelignore │ │ ├── composer.json │ │ ├── composer.lock │ │ ├── index.php │ │ └── vercel.json │ ├── 11-composer-env │ │ ├── .gitignore │ │ ├── .vercelignore │ │ ├── composer-test.json │ │ ├── composer-test.lock │ │ ├── index.php │ │ └── vercel.json │ ├── 12-composer │ │ ├── .gitignore │ │ ├── .vercelignore │ │ ├── api │ │ │ └── index.php │ │ ├── composer.json │ │ └── vercel.json │ ├── 13-composer-scripts │ │ ├── .gitignore │ │ ├── .vercelignore │ │ ├── api │ │ │ └── index.php │ │ ├── composer.json │ │ └── vercel.json │ ├── 14-folders │ │ ├── .gitignore │ │ ├── api │ │ │ ├── index.php │ │ │ └── users │ │ │ │ ├── index.php │ │ │ │ └── users.php │ │ └── vercel.json │ ├── 16-php-ini │ │ ├── .gitignore │ │ ├── api │ │ │ ├── index.php │ │ │ └── php.ini │ │ └── vercel.json │ ├── 17-zero │ │ ├── .gitignore │ │ ├── api │ │ │ ├── index.php │ │ │ └── test.html │ │ ├── src │ │ │ └── index.txt │ │ └── vercel.json │ ├── 18-exclude-files │ │ ├── .gitignore │ │ ├── .vercelignore │ │ ├── api │ │ │ └── index.php │ │ ├── baz │ │ │ └── index.html │ │ ├── foo │ │ │ └── index.txt │ │ └── vercel.json │ ├── 19-server-workers │ │ ├── .gitignore │ │ ├── api │ │ │ └── index.php │ │ └── vercel.json │ └── 20-read-files │ │ ├── .gitignore │ │ ├── api │ │ └── index.php │ │ ├── src │ │ └── users.json │ │ └── vercel.json └── spec │ ├── index.dev.js │ ├── index.js │ ├── launchers │ └── cgi.js │ ├── path.js │ └── url.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{html}] 12 | indent_style = tab 13 | indent_size = tab 14 | tab_width = 4 15 | 16 | [*.{js,ts,json,yml,yaml,md}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.github/.kodiak.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [merge] 4 | automerge_label = "automerge" 5 | blacklist_title_regex = "^WIP.*" 6 | blacklist_labels = ["WIP"] 7 | method = "rebase" 8 | delete_branch_on_merge = true 9 | notify_on_conflict = true 10 | optimistic_updates = false 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 🐛 3 | about: Something is not working as expected! 4 | --- 5 | 6 | # Bug report 7 | 8 | - Version: x.y.z 9 | - URL: Yes (*.now.sh) / No 10 | - Repository: Yes / No 11 | 12 | ## Description 13 | 14 | 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 🚀 3 | about: I would appreciate new feature or something! 4 | --- 5 | 6 | # Feature Request 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question ❓ 3 | about: Ask about anything! 4 | --- 5 | 6 | # Question 7 | 8 | 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: '11:00' 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: npm 10 | directory: "/packages/php" 11 | schedule: 12 | interval: daily 13 | time: '11:00' 14 | open-pull-requests-limit: 10 15 | - package-ecosystem: npm 16 | directory: "/packages/caddy" 17 | schedule: 18 | interval: daily 19 | time: '11:00' 20 | open-pull-requests-limit: 10 21 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main workflow 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | - name: Setup Node 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: '14.x' 16 | 17 | - name: Dependencies 18 | run: make install 19 | 20 | - name: Build 21 | run: make build 22 | 23 | - name: Tests 24 | run: make test 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node 2 | /node_modules 3 | /package-lock.json 4 | 5 | # App 6 | /dist 7 | 8 | # Vercel 9 | .vercel 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### [0.7.3] - 2024-10-19 4 | 5 | - Upgrade PHP 8.3 runtime (fixes for curl, opcache) 6 | 7 | ### [0.7.2] - 2024-09-30 8 | 9 | - Upgrade PHP 8.3 runtime 10 | 11 | ### [0.7.1] - 2024-04-16 12 | 13 | - Fix autodetect runtime 14 | 15 | ### [0.6.2] - 2024-04-16 16 | 17 | - Fix autodetect runtime 18 | 19 | ### [0.5.5] - 2024-04-16 20 | 21 | - Fix autodetect runtime 22 | 23 | ### [0.4.4] - 2024-04-16 24 | 25 | - Fix autodetect runtime 26 | 27 | ### [0.3.6] - 2024-04-16 28 | 29 | - Fix autodetect runtime 30 | 31 | ### [0.7.0] - 2024-02-22 32 | 33 | - PHP 8.3 34 | - Use `@libphp/amazon-linux-2-v83: latest` 35 | 36 | ### [0.6.1] - 2024-01-24 37 | 38 | - Update LD_LIBRARY_PATH 39 | 40 | ### [0.5.4] - 2024-01-24 41 | 42 | - Update LD_LIBRARY_PATH 43 | 44 | ### [0.4.3] - 2024-01-24 45 | 46 | - Update LD_LIBRARY_PATH 47 | 48 | ### [0.3.5] - 2024-01-24 49 | 50 | - Update LD_LIBRARY_PATH 51 | 52 | ### [0.6.0] - 2023-03-27 53 | 54 | - PHP 8.2 55 | - Use `@libphp/amazon-linux-2-v82: latest` 56 | 57 | ### [0.5.3] - 2023-03-27 58 | 59 | - Bump minimum node version from 14.x to 18.x 60 | - Upgrade dependencies 61 | 62 | ### [0.5.2] - 2022-08-10 63 | 64 | - Bump minimum node version from 12.x to 14.x 65 | 66 | ### [0.5.1] - 2022-05-05 67 | 68 | - Ignore .vercel folder during deployment 69 | 70 | ### [0.5.0] - 2022-04-09 71 | 72 | - PHP 8.1 73 | - Added extensions: geoip, zlib, zip 74 | - Removed extensions: psr 75 | - Use `@libphp/amazon-linux-2-v81: latest` 76 | 77 | ### [0.4.0] - 2021-01-02 78 | 79 | - PHP 8.0 80 | - Use `@libphp/amazon-linux-2-v80: latest` 81 | 82 | ### [0.3.2] - 2021-01-02 83 | 84 | - Typos 85 | - More hints in FAQ 86 | - Fix `excludeFiles` option 87 | - Install PHP extensions mongodb 88 | - Use `@libphp/amazon-linux-2-v74: latest` 89 | 90 | ### [0.3.1] - 2020-07-04 91 | 92 | - Install PHP extensions redis, msgpack, igbinary 93 | - Use `@libphp/amazon-linux-2-v74: latest` 94 | 95 | ### [0.3.0] - 2020-06-29 96 | 97 | - Allow to execute composer script called `vercel` 98 | 99 | ```json 100 | { 101 | "scripts": { 102 | "vercel": [ 103 | "@php -v", 104 | "npm -v" 105 | ] 106 | } 107 | } 108 | ``` 109 | 110 | - Drop support of `config['php.ini']` use `api/php.ini` file instead 111 | - Support excludeFiles (default `['node_modules/**', 'now.json', '.nowignore']`) 112 | 113 | ```json 114 | { 115 | "functions": { 116 | "api/**/*.php": { 117 | "runtime": "vercel-php@0.3.0", 118 | "excludeFiles": ["node_modules", "somedir", "foo/bar"], 119 | } 120 | } 121 | ``` 122 | 123 | - Restructure test folder (merge fixtures + my examples) 124 | 125 | ### [0.2.0] - 2020-06-26 126 | 127 | - Allow to override `php.ini` 128 | 129 | ```sh 130 | project 131 | ├── api 132 | │ ├── index.php 133 | │ └── php.ini 134 | └── now.json 135 | ``` 136 | 137 | - Extensive update of docs 138 | - Introduce FAQ questions 139 | - Move caddy package to [juicyfx/juicy](https://github.com/juicyfx/juicy) 140 | - Simplify repository structure 141 | 142 | ### [0.1.0] - 2020-06-20 143 | 144 | - Rename repository from now-php to **vercel-php** 145 | - Rename NPM package from now-php to **vercel-php** 146 | - Upgrade PHP to 7.4.7 and recompile PHP extensions 147 | - Improve readme 148 | - Separate PHP libs to solo repository [juicyfx/libphp](https://github.com/juicyfx/libphp) (bigger plans) 149 | - Use [php.vercel.app](https://php.vercel.app) domain for official showtime 150 | - Use [phpshow.vercel.app](https://phpshow.vercel.app) domain for runtime showcase 151 | 152 | ### [0.0.9] - 2020-03-28 153 | 154 | - Use PHP 7.4 for installing Composer dependencies 155 | - Upgrade PHP 7.4 and recompile PHP extensions 156 | 157 | ### [0.0.9] - 2020-01-16 158 | 159 | - Use PHP 7.3 for installing Composer dependencies 160 | - Separate [examples](https://github.com/juicyfx/vercel-examples) to solo repository 161 | - Extensions 162 | - Disabled ssh2 163 | - Added psr 164 | - Rebuild phalcon, swoole 165 | 166 | ### [0.0.8] - 2020-01-07 167 | 168 | - Runtime v3 169 | - Upgrade to PHP 7.4.x 170 | - Node 8.x reached EOL on AWS 171 | - Used Amazon Linux 2 172 | - CGI launcher inherits process.env [#38] 173 | - Drop Circle CI 174 | - Rebuild all PHP libs 175 | 176 | ### [0.0.7] - 2019-11-08 177 | 178 | - Rename builder to runtime 179 | - Runtime v3 180 | 181 | **Migration** 182 | 183 | ```json 184 | { 185 | "version": 2, 186 | "builds": [ 187 | { 188 | "src": "index.php", 189 | "use": "now-php" 190 | } 191 | ] 192 | } 193 | ``` 194 | 195 | ➡️ 196 | 197 | ```json 198 | { 199 | "functions": { 200 | "api/*.php": { 201 | "runtime": "now-php@0.0.7" 202 | } 203 | }, 204 | // Optionally provide routes 205 | "routes": [ 206 | { "src": "/(.*)", "dest": "/api/index.php" } 207 | ] 208 | } 209 | ``` 210 | 211 | ### [0.0.6] - 2019-11-07 212 | 213 | - Change builds to functions 214 | 215 | ### [0.0.5] - 2019-09-30 216 | 217 | - Added Lumen example 218 | - Bugfix deploying PHP files in folders under different names then index.php 219 | 220 | ### [0.0.4] - 2019-09-30 221 | 222 | - Implement intermediate caching (vendor, composer.lock, yarn.locak, package-lock.json, node_modules) 223 | - Rewrite PHP built-in server document root 224 | 225 | ### [0.0.3] - 2019-09-04 226 | 227 | - Bugfix passing query parameters and accessing $_GET 228 | 229 | ### [0.0.2] - 2019-08-23 230 | 231 | - Bump now-php@latest 232 | 233 | ### [0.0.1-canary.39] - 2019-08-23 234 | 235 | - Allow overriding php.ini 236 | - Bugfix resolving PHP bin 237 | - Bugfix deploying php files in subfolders 238 | 239 | ### [0.0.2-canary.2] - 2019-08-16 240 | 241 | - Compile PHP 7.3.8 242 | 243 | ### [0.0.1-canary.5] - 2019-08-16 244 | 245 | - First working copy of caddy server 246 | 247 | ### [0.0.1-canary.30] - 2019-08-16 248 | 249 | - New exported method `getPhpLibFiles` 250 | - Repair tests 251 | 252 | ### [0.0.1-canary.18] - 2019-08-02 253 | 254 | - Bump now-php@latest 255 | 256 | ### [0.0.1-canary.18] - 2019-08-02 257 | 258 | - Working on change response from string to Buffer 259 | - Updated homepage 260 | 261 | ### [0.0.1-canary.17] - 2019-08-02 262 | 263 | - Working on change response from string to Buffer 264 | 265 | ### [0.0.1-canary.15] - 2019-08-02 266 | 267 | - CGI: REQUEST_URI contains only path, not host + path 268 | - CGI: QUERY_STRING contains string without leading ? 269 | 270 | ### [0.0.1-canary.14] - 2019-07-29 271 | 272 | - Tests: more tests 273 | 274 | ### [0.0.1-canary.13] - 2019-07-29 275 | 276 | - Tests: take tests from official old builder 277 | 278 | ### [0.0.1-canary.12] - 2019-07-28 279 | 280 | - Rewritten to TypeScript 281 | 282 | ### [0.0.1-canary.11] - 2019-07-28 283 | 284 | - Working on `now-dev` 285 | 286 | ### [0.0.1-canary.8] - 2019-07-27 287 | 288 | - First working `now-php` builder 289 | 290 | ### [0.0.1-canary.7] - 2019-07-27 291 | 292 | - Working on `now` with `now-php` 293 | 294 | ### [0.0.1-canary.0] - 2019-07-27 295 | 296 | - History begins 297 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Juicy(fx) 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install build test publish canary 2 | 3 | install: 4 | npm install 5 | 6 | build: 7 | npm run build 8 | 9 | build-watch: 10 | npm run watch 11 | 12 | test: 13 | npm run test 14 | 15 | test-watch: 16 | npm run test-watch 17 | 18 | publish: 19 | rm -rf ./dist 20 | npm publish --access public --tag latest 21 | 22 | canary: 23 | rm -rf ./dist 24 | npm publish --access public --tag canary 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PHP Runtime for Vercel

2 | 3 |

4 | Enjoyable & powerful 🐘 PHP Runtime (php.vercel.app) for Vercel platform. 5 |

6 | 7 |

8 | 9 | 10 | 11 | 12 | 13 |

14 | 15 |

16 | 17 | 18 | 19 | 20 | 21 |

22 | 23 |

🏋️‍♀️ It works with these frameworks and tools. Discover more at examples.

24 | 25 |

26 | Made with ❤️ by @f3l1x (f3l1x.io) • 🐦 @xf3l1x 27 |

28 | 29 | ----- 30 | 31 | ## 😎 Getting Started 32 | 33 | Let's picture you want to deploy your awesome microproject written in PHP and you don't know where. You have found [Vercel](https://vercel.com) it's awesome, but for static sites. Not anymore! I would like to introduce you your new best friend `vercel-php`, PHP runtime for Vercel platform. 34 | 35 | Most simple example project is this one, using following project structure. 36 | 37 | ```sh 38 | project 39 | ├── api 40 | │ └── index.php 41 | └── vercel.json 42 | ``` 43 | 44 | First file `api/index.php` is entrypoint of our application. It should be placed in **api** folder, it's very standard location for Vercel. 45 | 46 | ```php 47 | 79 | 80 | ## 🤗 Features 81 | 82 | - **Architecture**: PHP development server (🚀 fast enough) 83 | - **PHP version**: 8.3 (https://example-php-8-3.vercel.app) 84 | - **Extensions**: apcu, bcmath, brotli, bz2, calendar, Core, ctype, curl, date, dom, ds, exif, fileinfo, filter, ftp, geoip, gettext, hash, iconv, igbinary, imap, intl, json, libxml, lua, mbstring, mongodb, msgpack, mysqli, mysqlnd, openssl, pcntl, pcre, PDO, pdo_mysql, pdo_pgsql, pdo_sqlite, pgsql, phalcon, Phar, protobuf, readline, redis, Reflection, runkit7, session, SimpleXML, soap, sockets, sodium, SPL, sqlite3, standard, swoole, timecop, tokenizer, uuid, xml, xmlreader, xmlrpc, xmlwriter, xsl, Zend OPcache, zlib, zip 85 | - **Speed**: cold ~250ms / warm ~5ms 86 | - **Memory**: ~90mb 87 | - **Frameworks**: Nette, Symfony, Lumen, Slim, Phalcon 88 | 89 | > List of all installable extensions is on this page https://blog.remirepo.net/pages/PECL-extensions-RPM-status. 90 | 91 | ## 💯 Versions 92 | 93 | - `vercel-php@0.7.3` - Node autodetect / PHP 8.3.x (https://example-php-8-3.vercel.app) 94 | - `vercel-php@0.6.2` - Node autodetect / PHP 8.2.x (https://example-php-8-2.vercel.app) 95 | - `vercel-php@0.5.5` - Node autodetect / PHP 8.1.x (https://example-php-8-1.vercel.app) 96 | - `vercel-php@0.4.4` - Node autodetect / PHP 8.0.x (https://example-php-8-0.vercel.app) 97 | - `vercel-php@0.3.6` - Node autodetect / PHP 7.4.x (https://example-php-7-4.vercel.app) 98 | 99 | ## ⚙️ Usage 100 | 101 | Before you can start using this runtime, you should learn about Vercel and [how runtimes](https://vercel.com/docs/runtimes?query=runtime#official-runtimes) works. Take a look at blogpost about [`Serverless Functions`](https://vercel.com/blog/customizing-serverless-functions). 102 | 103 | You should define `functions` property in `vercel.json` and list PHP files directly or using wildcard (*). 104 | If you need to route everything to index, use `routes` property. 105 | 106 | ```json 107 | { 108 | "functions": { 109 | "api/*.php": { 110 | "runtime": "vercel-php@0.7.3" 111 | } 112 | }, 113 | "routes": [ 114 | { "src": "/(.*)", "dest": "/api/index.php" } 115 | ] 116 | } 117 | ``` 118 | 119 | Do you have more questions (❓)? Let's move to [FAQ](#%EF%B8%8F-faq). 120 | 121 | ## 👨‍💻 `vercel dev` 122 | 123 | For running `vercel dev` properly, you need to have PHP installed on your computer, [learn more](errors/now-dev-no-local-php.md). 124 | But it's PHP and as you know PHP has built-in development server. It works out of box. 125 | 126 | ``` 127 | php -S localhost:8000 api/index.php 128 | ``` 129 | 130 | ## 👀 Demo 131 | 132 | - official - https://php.vercel.app/ 133 | - phpinfo - https://phpshow.vercel.app/ 134 | - extensions - https://phpshow.vercel.app/ext/ 135 | - ini - https://phpshow.vercel.app/ini/ 136 | - JSON API - https://phpshow.vercel.app/api/users.php 137 | - test - https://phpshow.vercel.app/test.php 138 | 139 | ![PHP](https://api.microlink.io?url=https://phpshow.vercel.app&screenshot&embed=screenshot.url) 140 | 141 | ## 🎯Examples 142 | 143 | - [PHP - fast & simple](https://github.com/juicyfx/vercel-examples/tree/master/php/) 144 | - [Composer - install dependencies](https://github.com/juicyfx/vercel-examples/tree/master/php-composer/) 145 | - [Framework - Laravel](https://github.com/juicyfx/vercel-examples/blob/master/php-laravel) 146 | - [Framework - Lumen](https://github.com/juicyfx/vercel-examples/blob/master/php-lumen) 147 | - [Framework - Nette](https://github.com/juicyfx/vercel-examples/blob/master/php-nette-tracy) 148 | - [Framework - Phalcon](https://github.com/juicyfx/vercel-examples/blob/master/php-phalcon) 149 | - [Framework - Slim](https://github.com/juicyfx/vercel-examples/blob/master/php-slim) 150 | - [Framework - Symfony - Microservice](https://github.com/juicyfx/vercel-examples/blob/master/php-symfony-microservice) 151 | 152 | Browse [more examples](https://github.com/juicyfx/vercel-examples). 👀 153 | 154 | ## 📜 Resources 155 | 156 | - [2019/10/23 - Code Examples](https://github.com/trainit/2019-10-hubbr-zeit) 157 | - [2019/10/19 - ZEIT - Deploy Serverless Microservices Right Now](https://slides.com/f3l1x/2019-10-19-zeit-deploy-serverless-microservices-right-now-vol2) 158 | - [2019/08/23 - Code Examples](https://github.com/trainit/2019-08-serverless-zeit-now) 159 | - [2019/07/07 - Bleeding Edge PHP on ZEIT Now](https://dev.to/nx1/bleeding-edge-php-on-zeit-now-565g) 160 | - [2019/06/06 - Code Examples](https://github.com/trainit/2019-06-zeit-now) 161 | - [2019/06/05 - ZEIT - Deploy Serverless Microservices Right Now](https://slides.com/f3l1x/2019-06-05-zeit-deploy-serverless-microservices-right-now) ([VIDEO](https://www.youtube.com/watch?v=IwhEGNDx3aE)) 162 | 163 | ## 🚧 Roadmap 164 | 165 | See [roadmap issue](https://github.com/juicyfx/vercel-php/issues/3). Help wanted. 166 | 167 | ## ⁉️ FAQ 168 | 169 |
170 | 1. How to use more then one endpoint (index.php)? 171 | 172 | ```sh 173 | project 174 | ├── api 175 | │ ├── index.php 176 | │ ├── users.php 177 | │ └── books.php 178 | └── vercel.json 179 | ``` 180 | 181 | ``` 182 | { 183 | "functions": { 184 | "api/*.php": { 185 | "runtime": "vercel-php@0.7.3" 186 | }, 187 | 188 | // Can be list also directly 189 | 190 | "api/index.php": { 191 | "runtime": "vercel-php@0.7.3" 192 | }, 193 | "api/users.php": { 194 | "runtime": "vercel-php@0.7.3" 195 | }, 196 | "api/books.php": { 197 | "runtime": "vercel-php@0.7.3" 198 | } 199 | } 200 | } 201 | ``` 202 | 203 |
204 | 205 |
206 | 2. How to route everything to index? 207 | 208 | ```json 209 | { 210 | "functions": { 211 | "api/index.php": { 212 | "runtime": "vercel-php@0.7.3" 213 | } 214 | }, 215 | "routes": [ 216 | { "src": "/(.*)", "dest": "/api/index.php" } 217 | ] 218 | } 219 | ``` 220 | 221 |
222 | 223 |
224 | 3. How to update memory limit? 225 | 226 | Additional function properties are `memory`, `maxDuration`. Learn more about [functions](https://vercel.com/docs/configuration#project/functions). 227 | 228 | ```json 229 | { 230 | "functions": { 231 | "api/*.php": { 232 | "runtime": "vercel-php@0.7.3", 233 | "memory": 3008, 234 | "maxDuration": 60 235 | } 236 | } 237 | } 238 | ``` 239 | 240 |
241 | 242 |
243 | 4. How to use it with Composer? 244 | 245 | Yes, [Composer](https://getcomposer.org/) is fully supported. 246 | 247 | ```sh 248 | project 249 | ├── api 250 | │ └── index.php 251 | ├── composer.json 252 | └── vercel.json 253 | ``` 254 | 255 | ```json 256 | { 257 | "functions": { 258 | "api/*.php": { 259 | "runtime": "vercel-php@0.7.3" 260 | } 261 | } 262 | } 263 | ``` 264 | 265 | ```json 266 | { 267 | "require": { 268 | "php": "^8.1", 269 | "tracy/tracy": "^2.0" 270 | } 271 | } 272 | ``` 273 | 274 | It's also good thing to create `.vercelignore` file and put `/vendor` folder to this file. It will not upload 275 | `/vendor` folder to Vercel platform. 276 | 277 |
278 | 279 |
280 | 5. How to override php.ini / php configuration ? 281 | 282 | Yes, you can override php configuration. Take a look at [default configuration](https://phpshow.vercel.app/) at first. 283 | Create a new file `api/php.ini` and place there your configuration. Don't worry, this particulary file will be 284 | removed during building phase on Vercel. 285 | 286 | ```sh 287 | project 288 | ├── api 289 | │ ├── index.php 290 | │ └── php.ini 291 | └── vercel.json 292 | ``` 293 | 294 | ```json 295 | { 296 | "functions": { 297 | "api/*.php": { 298 | "runtime": "vercel-php@0.7.3" 299 | } 300 | } 301 | } 302 | ``` 303 | 304 | ```json 305 | # Disable some functions 306 | disable_functions = "exec, system" 307 | 308 | # Update memory limit 309 | memory_limit=1024M 310 | ``` 311 | 312 |
313 | 314 |
315 | 6. How to exclude some files or folders ? 316 | 317 | Runtimes support excluding some files or folders, [take a look at doc](https://vercel.com/docs/configuration?query=excludeFiles#project/functions). 318 | 319 | ```json 320 | { 321 | "functions": { 322 | "api/**/*.php": { 323 | "runtime": "vercel-php@0.7.3", 324 | "excludeFiles": "{foo/**,bar/config/*.yaml}", 325 | } 326 | } 327 | ``` 328 | 329 | If you want to exclude files before uploading them to Vercel, use `.vercelignore` file. 330 | 331 |
332 | 333 |
334 | 7. How to call composer script(s) ? 335 | 336 | Calling composer scripts during build phase on Vercel is supported via script named `vercel`. You can easilly call php, npm or node. 337 | 338 | ```json 339 | { 340 | "require": { ... }, 341 | "require-dev": { ... }, 342 | "scripts": { 343 | "vercel": [ 344 | "@php -v", 345 | "npm -v" 346 | ] 347 | } 348 | } 349 | ``` 350 | 351 | Files created during `composer run vercel` script can be used (require/include) in your PHP lambdas, but can't be accessed from browser (like assets). If you still want to access them, create fake `assets.php` lambda and require them. [Example of PHP satis](https://github.com/juicyfx/vercel-examples/tree/master/php-satis). 352 | 353 |
354 | 355 |
356 | 8. How to include some files of folders? 357 | 358 | If you are looking for [`config.includeFiles`](https://vercel.com/docs/configuration?query=includeFiles#project/functions) in runtime, unfortunately you can't include extra files. 359 | All files in root folder are uploaded to Vercel, use `.vercelignore` to exclude them before upload. 360 | 361 |
362 | 363 |
364 | 9. How to develop locally? 365 | 366 | I think the best way at this moment is use [PHP Development Server](https://www.php.net/manual/en/features.commandline.webserver.php). 367 | 368 | ``` 369 | php -S localhost:8000 api/index.php 370 | ``` 371 | 372 |
373 | 374 | ## 👨🏻‍💻CHANGELOG 375 | 376 | Show me [CHANGELOG](./CHANGELOG.md) 377 | 378 | ## 🧙Contribution 379 | 380 | 1. Clone this repository. 381 | - `git clone git@github.com:juicyfx/vercel-php.git` 382 | 2. Install NPM dependencies 383 | - `make install` 384 | 3. Make your changes 385 | 4. Run TypeScript compiler 386 | - `make build` 387 | 5. Run tests 388 | - `make test` 389 | 6. Create a PR 390 | 391 | ## 📝 License 392 | 393 | Copyright © 2019 [f3l1x](https://github.com/f3l1x). 394 | This project is [MIT](LICENSE) licensed. 395 | -------------------------------------------------------------------------------- /conf/build.ini: -------------------------------------------------------------------------------- 1 | ; Override for build phase on Vercel 2 | extension_dir = "${PHP_INI_EXTENSION_DIR}" 3 | -------------------------------------------------------------------------------- /errors/now-dev-no-local-php.md: -------------------------------------------------------------------------------- 1 | # It looks like you don't have PHP on your machine. 2 | 3 | **Why This Error Occurred** 4 | 5 | You ran `now dev` on a machine where PHP is not installed. 6 | For the time being, this runtime requires a local PHP installation to run the runtime locally. 7 | 8 | **Possible Ways to Fix It** 9 | 10 | 1. Install PHP to your computer 11 | 12 | **OSX** 13 | 14 | ``` 15 | brew install php@7.4 16 | ``` 17 | 18 | **Ubuntu** 19 | 20 | ``` 21 | apt-get -y install apt-transport-https lsb-release ca-certificates 22 | wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg 23 | sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' 24 | apt-get update 25 | apt-get install php7.4-cli php7.4-cgi php7.4-json php7.4-curl php7.4-mbstring 26 | ``` 27 | 28 | **Fedora** 29 | 30 | ``` 31 | yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 32 | yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm 33 | yum install yum-utils 34 | yum-config-manager --enable remi-php74 35 | yum update 36 | yum install php74-cli php74-cgi php74-json php74-curl php74-mbstring 37 | ``` 38 | 39 | 2. Start PHP built-in Development Server 40 | 41 | ```sh 42 | php -S localhost:8000 api/index.php 43 | ``` 44 | 45 | **Check that php is in the path** 46 | 47 | If you do have installed PHP but still get this error, check that PHP executable is added to the PATH environment variable. 48 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: ".", 3 | verbose: true, 4 | testEnvironment: "node", 5 | testMatch: [ 6 | "**/test/spec/**/*.js", 7 | ], 8 | testPathIgnorePatterns: [ 9 | "/errors/", 10 | "/dist/", 11 | "/node_modules/", 12 | ], 13 | testTimeout: 10000 14 | } 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vercel-php", 3 | "description": "Vercel PHP runtime", 4 | "version": "0.7.3", 5 | "license": "MIT", 6 | "main": "./dist/index.js", 7 | "homepage": "https://github.com/juicyfx/vercel-php", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/juicyfx/vercel-php.git" 11 | }, 12 | "keywords": [ 13 | "vercel", 14 | "zeit", 15 | "now", 16 | "php", 17 | "builder", 18 | "runtime", 19 | "serverless", 20 | "deployment" 21 | ], 22 | "scripts": { 23 | "watch": "tsc --watch", 24 | "build": "tsc", 25 | "test": "jest --silent", 26 | "test-watch": "jest --watch", 27 | "prepublishOnly": "tsc" 28 | }, 29 | "files": [ 30 | "dist", 31 | "conf" 32 | ], 33 | "dependencies": { 34 | "@libphp/amazon-linux-2-v83": ">=0.0.7" 35 | }, 36 | "devDependencies": { 37 | "@types/glob": "8.1.0", 38 | "@types/node": "18.19.18", 39 | "@vercel/build-utils": "7.11.0", 40 | "jest": "29.7.0", 41 | "typescript": "5.4.5" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { 3 | rename, 4 | shouldServe, 5 | glob, 6 | download, 7 | Lambda, 8 | BuildV3, 9 | PrepareCache, 10 | getNodeVersion 11 | } from '@vercel/build-utils'; 12 | import { 13 | getPhpFiles, 14 | getLauncherFiles, 15 | runComposerInstall, 16 | runComposerScripts, 17 | readRuntimeFile, 18 | modifyPhpIni, 19 | } from './utils'; 20 | 21 | const COMPOSER_FILE = process.env.COMPOSER || 'composer.json'; 22 | 23 | // ########################### 24 | // EXPORTS 25 | // ########################### 26 | 27 | export const version = 3; 28 | 29 | export const build: BuildV3 = async ({ 30 | files, 31 | entrypoint, 32 | workPath, 33 | config = {}, 34 | meta = {}, 35 | }) => { 36 | // Check if now dev mode is used 37 | if (meta.isDev) { 38 | console.log(` 39 | 🐘 vercel dev is not supported right now. 40 | Please use PHP built-in development server. 41 | 42 | php -S localhost:8000 api/index.php 43 | `); 44 | process.exit(255); 45 | } 46 | 47 | console.log('🐘 Downloading user files'); 48 | 49 | // Collect user provided files 50 | const userFiles: RuntimeFiles = await download(files, workPath, meta); 51 | 52 | console.log('🐘 Downloading PHP runtime files'); 53 | 54 | // Collect runtime files containing PHP bins and libs 55 | const runtimeFiles: RuntimeFiles = { 56 | // Append PHP files (bins + shared object) 57 | ...await getPhpFiles(), 58 | 59 | // Append launcher files (builtin server, common helpers) 60 | ...getLauncherFiles(), 61 | }; 62 | 63 | // If composer.json is provided try to 64 | // - install deps 65 | // - run composer scripts 66 | if (userFiles[COMPOSER_FILE]) { 67 | // Install dependencies (vendor is collected bellow, see harvestedFiles) 68 | await runComposerInstall(workPath); 69 | 70 | // Run composer scripts (created files are collected bellow, , see harvestedFiles) 71 | await runComposerScripts(userFiles[COMPOSER_FILE], workPath); 72 | } 73 | 74 | // Append PHP directives into php.ini 75 | if (userFiles['api/php.ini']) { 76 | const phpini = await modifyPhpIni(userFiles, runtimeFiles); 77 | if (phpini) { 78 | runtimeFiles['php/php.ini'] = phpini; 79 | } 80 | } 81 | 82 | // Collect user files, files creating during build (composer vendor) 83 | // and other files and prefix them with "user" (/var/task/user folder). 84 | const harverstedFiles = rename( 85 | await glob('**', { 86 | cwd: workPath, 87 | ignore: [ 88 | '.vercel/**', 89 | ...(config?.excludeFiles 90 | ? Array.isArray(config.excludeFiles) 91 | ? config.excludeFiles 92 | : [config.excludeFiles] 93 | : [ 94 | 'node_modules/**', 95 | 'now.json', 96 | '.nowignore', 97 | 'vercel.json', 98 | '.vercelignore', 99 | ]), 100 | ], 101 | }), 102 | name => path.join('user', name) 103 | ); 104 | 105 | // Show some debug notes during build 106 | if (process.env.NOW_PHP_DEBUG === '1') { 107 | console.log('🐘 Entrypoint:', entrypoint); 108 | console.log('🐘 Config:', config); 109 | console.log('🐘 Work path:', workPath); 110 | console.log('🐘 Meta:', meta); 111 | console.log('🐘 User files:', Object.keys(harverstedFiles)); 112 | console.log('🐘 Runtime files:', Object.keys(runtimeFiles)); 113 | console.log('🐘 PHP: php.ini', await readRuntimeFile(runtimeFiles['php/php.ini'])); 114 | } 115 | 116 | console.log('🐘 Creating lambda'); 117 | const nodeVersion = await getNodeVersion(workPath); 118 | 119 | const lambda = new Lambda({ 120 | files: { 121 | // Located at /var/task/user 122 | ...harverstedFiles, 123 | // Located at /var/task/php (php bins + ini + modules) 124 | // Located at /var/task/lib (shared libs) 125 | ...runtimeFiles 126 | }, 127 | handler: 'launcher.launcher', 128 | runtime: nodeVersion.runtime, 129 | environment: { 130 | NOW_ENTRYPOINT: entrypoint, 131 | NOW_PHP_DEV: meta.isDev ? '1' : '0' 132 | }, 133 | }); 134 | 135 | return { output: lambda }; 136 | }; 137 | 138 | export const prepareCache: PrepareCache = async ({ workPath }) => { 139 | return { 140 | // Composer 141 | ...(await glob('vendor/**', workPath)), 142 | ...(await glob('composer.lock', workPath)), 143 | // NPM 144 | ...(await glob('node_modules/**', workPath)), 145 | ...(await glob('package-lock.json', workPath)), 146 | ...(await glob('yarn.lock', workPath)), 147 | }; 148 | }; 149 | 150 | export { shouldServe }; 151 | -------------------------------------------------------------------------------- /src/launchers/builtin.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import { spawn, ChildProcess, SpawnOptions } from 'child_process'; 3 | import net from 'net'; 4 | import { 5 | getPhpDir, 6 | getUserDir, 7 | normalizeEvent, 8 | transformFromAwsRequest, 9 | transformToAwsResponse, 10 | isDev 11 | } from './helpers'; 12 | import { join as pathJoin } from 'path'; 13 | 14 | let server: ChildProcess; 15 | 16 | async function startServer(entrypoint: string): Promise { 17 | // Resolve document root and router 18 | const router = entrypoint; 19 | const docroot = pathJoin(getUserDir(), process.env.VERCEL_PHP_DOCROOT ?? ''); 20 | 21 | console.log(`🐘 Spawning: PHP Built-In Server at ${docroot} (document root) and ${router} (router)`); 22 | 23 | // php spawn options 24 | const options: SpawnOptions = { 25 | stdio: ['pipe', 'pipe', 'pipe'], 26 | env: { 27 | ...process.env, 28 | LD_LIBRARY_PATH: `/var/task/lib:${process.env.LD_LIBRARY_PATH}` 29 | } 30 | }; 31 | 32 | // now vs now-dev 33 | if (!isDev()) { 34 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; 35 | options.cwd = getPhpDir(); 36 | } else { 37 | options.cwd = getUserDir(); 38 | } 39 | 40 | // We need to start PHP built-in server with following setup: 41 | // php -c php.ini -S ip:port -t /var/task/user /var/task/user/foo/bar.php 42 | // 43 | // Path to document root lambda task folder with user prefix, because we move all 44 | // user files to this folder. 45 | // 46 | // Path to router is absolute path, because CWD is different. 47 | // 48 | server = spawn( 49 | 'php', 50 | ['-c', 'php.ini', '-S', '0.0.0.0:3000', '-t', docroot, router], 51 | options, 52 | ); 53 | 54 | server.stdout?.on('data', data => { 55 | console.log(`🐘STDOUT: ${data.toString()}`); 56 | }); 57 | 58 | server.stderr?.on('data', data => { 59 | console.error(`🐘STDERR: ${data.toString()}`); 60 | }); 61 | 62 | server.on('close', function (code, signal) { 63 | console.log(`🐘 PHP Built-In Server process closed code ${code} and signal ${signal}`); 64 | }); 65 | 66 | server.on('error', function (err) { 67 | console.error(`🐘 PHP Built-In Server process errored ${err}`); 68 | }); 69 | 70 | await whenPortOpens(3000, 500); 71 | 72 | process.on('exit', () => { 73 | server.kill(); 74 | }) 75 | 76 | return server; 77 | } 78 | 79 | async function query({ entrypoint, uri, path, headers, method, body }: PhpInput): Promise { 80 | if (!server) { 81 | await startServer(entrypoint); 82 | } 83 | 84 | return new Promise(resolve => { 85 | const options = { 86 | hostname: 'localhost', 87 | port: 3000, 88 | path, 89 | method, 90 | headers, 91 | }; 92 | 93 | console.log(`🐘 Accessing ${uri}`); 94 | console.log(`🐘 Querying ${path}`); 95 | 96 | const req = http.request(options, (res) => { 97 | const chunks: Uint8Array[] = []; 98 | 99 | res.on('data', (data) => { 100 | chunks.push(data); 101 | }); 102 | res.on('end', () => { 103 | resolve({ 104 | statusCode: res.statusCode || 200, 105 | headers: res.headers, 106 | body: Buffer.concat(chunks) 107 | }); 108 | }); 109 | }); 110 | 111 | req.on('error', (error) => { 112 | console.error('🐘 PHP Built-In Server HTTP errored', error); 113 | resolve({ 114 | body: Buffer.from(`PHP Built-In Server HTTP error: ${error}`), 115 | headers: {}, 116 | statusCode: 500 117 | }); 118 | }); 119 | 120 | if (body) { 121 | req.write(body); 122 | } 123 | 124 | req.end(); 125 | }); 126 | } 127 | 128 | function whenPortOpensCallback(port: number, attempts: number, cb: (error?: string) => void) { 129 | const client = net.connect(port, '127.0.0.1'); 130 | client.on('error', (error: string) => { 131 | if (!attempts) return cb(error); 132 | setTimeout(() => { 133 | whenPortOpensCallback(port, attempts - 1, cb); 134 | }, 10); 135 | }); 136 | client.on('connect', () => { 137 | client.destroy(); 138 | cb(); 139 | }); 140 | } 141 | 142 | function whenPortOpens(port: number, attempts: number): Promise { 143 | return new Promise((resolve, reject) => { 144 | whenPortOpensCallback(port, attempts, (error?: string) => { 145 | if (error) { 146 | reject(error); 147 | } else { 148 | resolve(); 149 | } 150 | }); 151 | }); 152 | } 153 | 154 | async function launcher(event: Event): Promise { 155 | const awsRequest = normalizeEvent(event); 156 | const input = await transformFromAwsRequest(awsRequest); 157 | const output = await query(input); 158 | return transformToAwsResponse(output); 159 | } 160 | 161 | exports.launcher = launcher; 162 | 163 | // (async function () { 164 | // const response = await launcher({ 165 | // Action: "test", 166 | // httpMethod: "GET", 167 | // body: "", 168 | // path: "/", 169 | // host: "https://vercel.com", 170 | // headers: { 171 | // 'HOST': 'vercel.com' 172 | // }, 173 | // encoding: null, 174 | // }); 175 | 176 | // console.log(response); 177 | // })(); 178 | -------------------------------------------------------------------------------- /src/launchers/cgi.ts: -------------------------------------------------------------------------------- 1 | import { spawn, SpawnOptions } from 'child_process'; 2 | import { parse as urlParse } from 'url'; 3 | import { 4 | getPhpDir, 5 | getUserDir, 6 | normalizeEvent, 7 | transformFromAwsRequest, 8 | transformToAwsResponse, 9 | isDev 10 | } from './helpers'; 11 | 12 | function createCGIReq({ entrypoint, path, host, method, headers }: CgiInput): CgiRequest { 13 | const { query } = urlParse(path); 14 | 15 | const env: Env = { 16 | ...process.env, 17 | SERVER_ROOT: getUserDir(), 18 | DOCUMENT_ROOT: getUserDir(), 19 | SERVER_NAME: host, 20 | SERVER_PORT: 443, 21 | HTTPS: "On", 22 | REDIRECT_STATUS: 200, 23 | SCRIPT_NAME: entrypoint, 24 | REQUEST_URI: path, 25 | SCRIPT_FILENAME: entrypoint, 26 | PATH_TRANSLATED: entrypoint, 27 | REQUEST_METHOD: method, 28 | QUERY_STRING: query || '', 29 | GATEWAY_INTERFACE: "CGI/1.1", 30 | SERVER_PROTOCOL: "HTTP/1.1", 31 | PATH: process.env.PATH, 32 | SERVER_SOFTWARE: "Vercel PHP", 33 | LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH 34 | }; 35 | 36 | if (headers["content-length"]) { 37 | env.CONTENT_LENGTH = headers["content-length"]; 38 | } 39 | 40 | if (headers["content-type"]) { 41 | env.CONTENT_TYPE = headers["content-type"]; 42 | } 43 | 44 | if (headers["x-real-ip"]) { 45 | env.REMOTE_ADDR = headers["x-real-ip"]; 46 | } 47 | 48 | // expose request headers 49 | Object.keys(headers).forEach(function (header) { 50 | var name = "HTTP_" + header.toUpperCase().replace(/-/g, "_"); 51 | env[name] = headers[header]; 52 | }); 53 | 54 | return { 55 | env 56 | } 57 | } 58 | 59 | function parseCGIResponse(response: Buffer) { 60 | const headersPos = response.indexOf("\r\n\r\n"); 61 | if (headersPos === -1) { 62 | return { 63 | headers: {}, 64 | body: response, 65 | statusCode: 200 66 | } 67 | } 68 | 69 | let statusCode = 200; 70 | const rawHeaders = response.slice(0, headersPos).toString(); 71 | const rawBody = response.slice(headersPos + 4); 72 | 73 | const headers = parseCGIHeaders(rawHeaders); 74 | 75 | if (headers['status']) { 76 | statusCode = parseInt(headers['status']) || 200; 77 | } 78 | 79 | return { 80 | headers, 81 | body: rawBody, 82 | statusCode 83 | } 84 | } 85 | 86 | function parseCGIHeaders(headers: string): CgiHeaders { 87 | if (!headers) return {} 88 | 89 | const result: CgiHeaders = {} 90 | 91 | for (let header of headers.split("\n")) { 92 | const index = header.indexOf(':'); 93 | const key = header.slice(0, index).trim().toLowerCase(); 94 | const value = header.slice(index + 1).trim(); 95 | 96 | // Be careful about header duplication 97 | result[key] = value; 98 | } 99 | 100 | return result 101 | } 102 | 103 | function query({ entrypoint, path, host, headers, method, body }: PhpInput): Promise { 104 | console.log(`🐘 Spawning: PHP CGI ${entrypoint}`); 105 | 106 | // Transform lambda request to CGI variables 107 | const { env } = createCGIReq({ entrypoint, path, host, headers, method }) 108 | 109 | // php-cgi spawn options 110 | const options: SpawnOptions = { 111 | stdio: ['pipe', 'pipe', 'pipe'], 112 | env: env 113 | }; 114 | 115 | // now vs now-dev 116 | if (!isDev()) { 117 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; 118 | options.cwd = getPhpDir(); 119 | } else { 120 | options.cwd = getUserDir(); 121 | } 122 | 123 | return new Promise((resolve) => { 124 | const chunks: Uint8Array[] = []; 125 | 126 | const php = spawn( 127 | 'php-cgi', 128 | [entrypoint], 129 | options, 130 | ); 131 | 132 | // Validate pipes [stdin] 133 | if (!php.stdin) { 134 | console.error(`🐘 Fatal error. PHP CGI child process has no stdin.`); 135 | process.exit(253); 136 | } 137 | 138 | // Validate pipes [stdout] 139 | if (!php.stdout) { 140 | console.error(`🐘 Fatal error. PHP CGI child process has no stdout.`); 141 | process.exit(254); 142 | } 143 | 144 | // Validate pipes [stderr] 145 | if (!php.stderr) { 146 | console.error(`🐘 Fatal error. PHP CGI child process has no stderr.`); 147 | process.exit(255); 148 | } 149 | 150 | // Output 151 | php.stdout.on('data', data => { 152 | chunks.push(data); 153 | }); 154 | 155 | // Logging 156 | php.stderr.on('data', data => { 157 | console.error(`🐘 PHP CGI stderr`, data.toString()); 158 | }); 159 | 160 | // PHP script execution end 161 | php.on('close', (code, signal) => { 162 | if (code !== 0) { 163 | console.log(`🐘 PHP CGI process closed code ${code} and signal ${signal}`); 164 | } 165 | 166 | const { headers, body, statusCode } = parseCGIResponse(Buffer.concat(chunks)); 167 | 168 | resolve({ 169 | body, 170 | headers, 171 | statusCode 172 | }); 173 | }); 174 | 175 | php.on('error', err => { 176 | console.error('🐘 PHP CGI errored', err); 177 | resolve({ 178 | body: Buffer.from(`🐘 PHP CGI process errored ${err}`), 179 | headers: {}, 180 | statusCode: 500 181 | }); 182 | }); 183 | 184 | // Writes the body into the PHP stdin 185 | php.stdin.write(body || ''); 186 | php.stdin.end(); 187 | }) 188 | } 189 | 190 | async function launcher(event: Event): Promise { 191 | const awsRequest = normalizeEvent(event); 192 | const input = await transformFromAwsRequest(awsRequest); 193 | const output = await query(input); 194 | return transformToAwsResponse(output); 195 | } 196 | 197 | exports.createCGIReq = createCGIReq; 198 | exports.launcher = launcher; 199 | 200 | // (async function () { 201 | // const response = await launcher({ 202 | // Action: "test", 203 | // httpMethod: "GET", 204 | // body: "", 205 | // path: "/", 206 | // host: "https://vercel.com", 207 | // headers: { 208 | // 'HOST': 'vercel.com' 209 | // }, 210 | // encoding: null, 211 | // }); 212 | 213 | // console.log(response); 214 | // })(); 215 | -------------------------------------------------------------------------------- /src/launchers/cli.ts: -------------------------------------------------------------------------------- 1 | import { spawn, SpawnOptions } from 'child_process'; 2 | import { 3 | getPhpDir, 4 | normalizeEvent, 5 | transformFromAwsRequest, 6 | transformToAwsResponse, 7 | isDev, 8 | getUserDir 9 | } from './helpers'; 10 | 11 | function query({ entrypoint, body }: PhpInput): Promise { 12 | console.log(`🐘 Spawning: PHP CLI ${entrypoint}`); 13 | 14 | // php spawn options 15 | const options: SpawnOptions = { 16 | stdio: ['pipe', 'pipe', 'pipe'], 17 | env: process.env 18 | }; 19 | 20 | // now vs now-dev 21 | if (!isDev()) { 22 | options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; 23 | options.cwd = getPhpDir(); 24 | } else { 25 | options.cwd = getUserDir(); 26 | } 27 | 28 | return new Promise((resolve) => { 29 | const chunks: Uint8Array[] = []; 30 | 31 | const php = spawn( 32 | 'php', 33 | ['-c', 'php.ini', entrypoint], 34 | options, 35 | ); 36 | 37 | // Validate pipes [stdin] 38 | if (!php.stdin) { 39 | console.error(`🐘 Fatal error. PHP CLI child process has no stdin.`); 40 | process.exit(253); 41 | } 42 | 43 | // Validate pipes [stdout] 44 | if (!php.stdout) { 45 | console.error(`🐘 Fatal error. PHP CLI child process has no stdout.`); 46 | process.exit(254); 47 | } 48 | 49 | // Validate pipes [stderr] 50 | if (!php.stderr) { 51 | console.error(`🐘 Fatal error. PHP CLI child process has no stderr.`); 52 | process.exit(255); 53 | } 54 | 55 | // Output 56 | php.stdout.on('data', data => { 57 | chunks.push(data); 58 | }); 59 | 60 | // Logging 61 | php.stderr.on('data', data => { 62 | console.error(`🐘 PHP CLI stderr`, data.toString()); 63 | }); 64 | 65 | // PHP script execution end 66 | php.on('close', (code, signal) => { 67 | if (code !== 0) { 68 | console.log(`🐘 PHP CLI process closed code ${code} and signal ${signal}`); 69 | } 70 | 71 | resolve({ 72 | statusCode: 200, 73 | headers: {}, 74 | body: Buffer.concat(chunks) 75 | }); 76 | }); 77 | 78 | php.on('error', err => { 79 | console.error('🐘 PHP CLI errored', err); 80 | resolve({ 81 | body: Buffer.from(`🐘 PHP CLI process errored ${err}`), 82 | headers: {}, 83 | statusCode: 500 84 | }); 85 | }); 86 | 87 | // Writes the body into the PHP stdin 88 | php.stdin.write(body || ''); 89 | php.stdin.end(); 90 | }) 91 | } 92 | 93 | async function launcher(event: Event): Promise { 94 | const awsRequest = normalizeEvent(event); 95 | const input = await transformFromAwsRequest(awsRequest); 96 | const output = await query(input); 97 | return transformToAwsResponse(output); 98 | } 99 | 100 | exports.launcher = launcher; 101 | 102 | // (async function () { 103 | // const response = await launcher({ 104 | // Action: "test", 105 | // httpMethod: "GET", 106 | // body: "", 107 | // path: "/", 108 | // host: "https://vercel.com", 109 | // headers: { 110 | // 'HOST': 'vercel.com' 111 | // }, 112 | // encoding: null, 113 | // }); 114 | 115 | // console.log(response); 116 | // })(); 117 | -------------------------------------------------------------------------------- /src/launchers/helpers.ts: -------------------------------------------------------------------------------- 1 | import { join as pathJoin } from 'path'; 2 | 3 | export const getUserDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'user'); 4 | export const getPhpDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'php'); 5 | export const isDev = (): boolean => process.env.NOW_PHP_DEV === '1'; 6 | 7 | export function normalizeEvent(event: Event): AwsRequest { 8 | if (event.Action === 'Invoke') { 9 | const invokeEvent = JSON.parse(event.body); 10 | 11 | const { 12 | method, path, host, headers = {}, encoding, 13 | }: InvokedEvent = invokeEvent; 14 | 15 | let { body } = invokeEvent; 16 | 17 | if (body) { 18 | if (encoding === 'base64') { 19 | body = Buffer.from(body, encoding); 20 | } else if (encoding === undefined) { 21 | body = Buffer.from(body); 22 | } else { 23 | throw new Error(`Unsupported encoding: ${encoding}`); 24 | } 25 | } 26 | 27 | return { 28 | method, 29 | path, 30 | host, 31 | headers, 32 | body, 33 | }; 34 | } 35 | 36 | const { 37 | httpMethod: method, path, host, headers = {}, body, 38 | } = event; 39 | 40 | return { 41 | method, 42 | path, 43 | host, 44 | headers, 45 | body, 46 | }; 47 | } 48 | 49 | export async function transformFromAwsRequest({ 50 | method, path, host, headers, body, 51 | }: AwsRequest): Promise { 52 | if (!process.env.NOW_ENTRYPOINT) { 53 | console.error('Missing ENV NOW_ENTRYPOINT'); 54 | } 55 | 56 | const entrypoint = pathJoin( 57 | getUserDir(), 58 | process.env.NOW_ENTRYPOINT || 'index.php', 59 | ); 60 | 61 | const uri = host + path; 62 | 63 | return { entrypoint, uri, path, host, method, headers, body }; 64 | } 65 | 66 | export function transformToAwsResponse({ statusCode, headers, body }: PhpOutput): AwsResponse { 67 | return { statusCode, headers, body: body.toString('base64'), encoding: 'base64' }; 68 | } 69 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | type Headers = { [k: string]: string | string[] | undefined }; 2 | 3 | interface UserFiles { 4 | [filePath: string]: import('@vercel/build-utils').File; 5 | } 6 | 7 | interface RuntimeFiles { 8 | [filePath: string]: import('@vercel/build-utils').File; 9 | } 10 | 11 | interface IncludedFiles { 12 | [filePath: string]: import('@vercel/build-utils').File; 13 | } 14 | 15 | interface MetaOptions { 16 | meta: import('@vercel/build-utils').Meta; 17 | } 18 | 19 | interface AwsRequest { 20 | method: string, 21 | path: string, 22 | host: string, 23 | headers: Headers, 24 | body: string, 25 | } 26 | 27 | interface AwsResponse { 28 | statusCode: number, 29 | headers: Headers, 30 | body: string, 31 | encoding?: string 32 | } 33 | 34 | interface Event { 35 | Action: string, 36 | body: string, 37 | httpMethod: string, 38 | path: string, 39 | host: string, 40 | headers: Headers, 41 | encoding: string | undefined | null, 42 | } 43 | 44 | interface InvokedEvent { 45 | method: string, 46 | path: string, 47 | host: string, 48 | headers: Headers, 49 | encoding: string | undefined | null, 50 | } 51 | 52 | interface CgiInput { 53 | entrypoint: string, 54 | path: string, 55 | host: string, 56 | method: string, 57 | headers: Headers, 58 | } 59 | 60 | interface PhpInput { 61 | entrypoint: string, 62 | path: string, 63 | uri: string, 64 | host: string, 65 | method: string, 66 | headers: Headers, 67 | body: string, 68 | } 69 | 70 | interface PhpOutput { 71 | statusCode: number, 72 | headers: Headers, 73 | body: Buffer, 74 | } 75 | 76 | interface CgiHeaders { 77 | [k: string]: string, 78 | } 79 | 80 | interface CgiRequest { 81 | env: Env, 82 | } 83 | 84 | interface Env { 85 | [k: string]: any, 86 | } 87 | 88 | interface PhpIni { 89 | [k: string]: any, 90 | } 91 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { spawn, SpawnOptions } from 'child_process'; 3 | import { File, FileFsRef, FileBlob } from '@vercel/build-utils'; 4 | import * as libphp from "@libphp/amazon-linux-2-v83"; 5 | 6 | const PHP_PKG = path.dirname(require.resolve('@libphp/amazon-linux-2-v83/package.json')); 7 | const PHP_BIN_DIR = path.join(PHP_PKG, "native/php"); 8 | const PHP_MODULES_DIR = path.join(PHP_BIN_DIR, "modules"); 9 | const PHP_LIB_DIR = path.join(PHP_PKG, "native/lib"); 10 | const COMPOSER_BIN = path.join(PHP_BIN_DIR, "composer"); 11 | 12 | export async function getPhpFiles(): Promise { 13 | const files = await libphp.getFiles(); 14 | 15 | // Drop CGI + FPM from libphp, it's not needed for our case 16 | delete files['php/php-cgi']; 17 | delete files['php/php-fpm']; 18 | delete files['php/php-fpm.ini']; 19 | 20 | const runtimeFiles: RuntimeFiles = {}; 21 | 22 | // Map from @libphp to Vercel's File objects 23 | for (const [filename, filepath] of Object.entries(files)) { 24 | runtimeFiles[filename] = new FileFsRef({ 25 | fsPath: filepath 26 | }) 27 | } 28 | 29 | // Set some bins executable 30 | (runtimeFiles['php/php'] as FileFsRef).mode = 33261; // 0755; 31 | (runtimeFiles['php/composer'] as FileFsRef).mode = 33261; // 0755; 32 | 33 | return runtimeFiles; 34 | } 35 | 36 | export function getLauncherFiles(): RuntimeFiles { 37 | const files: RuntimeFiles = { 38 | 'helpers.js': new FileFsRef({ 39 | fsPath: path.join(__dirname, 'launchers/helpers.js'), 40 | }) 41 | } 42 | 43 | files['launcher.js'] = new FileFsRef({ 44 | fsPath: path.join(__dirname, 'launchers/builtin.js'), 45 | }); 46 | 47 | return files; 48 | } 49 | 50 | export async function modifyPhpIni(userFiles: UserFiles, runtimeFiles: RuntimeFiles): Promise { 51 | // Validate user files contains php.ini 52 | if (!userFiles['api/php.ini']) return; 53 | 54 | // Validate runtime contains php.ini 55 | if (!runtimeFiles['php/php.ini']) return; 56 | 57 | const phpiniBlob = await FileBlob.fromStream({ 58 | stream: runtimeFiles['php/php.ini'].toStream(), 59 | }); 60 | 61 | const userPhpiniBlob = await FileBlob.fromStream({ 62 | stream: userFiles['api/php.ini'].toStream(), 63 | }); 64 | 65 | return new FileBlob({ 66 | data: phpiniBlob.data.toString() 67 | .concat("; [User]\n") 68 | .concat(userPhpiniBlob.data.toString()) 69 | }); 70 | } 71 | 72 | export async function runComposerInstall(workPath: string): Promise { 73 | console.log('🐘 Installing Composer dependencies [START]'); 74 | 75 | // @todo PHP_COMPOSER_INSTALL env 76 | await runPhp( 77 | [ 78 | COMPOSER_BIN, 79 | 'install', 80 | '--profile', 81 | '--no-dev', 82 | '--no-interaction', 83 | '--no-scripts', 84 | '--ignore-platform-reqs', 85 | '--no-progress' 86 | ], 87 | { 88 | stdio: 'inherit', 89 | cwd: workPath 90 | } 91 | ); 92 | 93 | console.log('🐘 Installing Composer dependencies [DONE]'); 94 | } 95 | 96 | export async function runComposerScripts(composerFile: File, workPath: string): Promise { 97 | let composer; 98 | 99 | try { 100 | composer = JSON.parse(await readRuntimeFile(composerFile)); 101 | } catch (e) { 102 | console.error('🐘 Composer file is not valid JSON'); 103 | console.error(e); 104 | return; 105 | } 106 | 107 | if (composer?.scripts?.vercel) { 108 | console.log('🐘 Running composer scripts [START]'); 109 | 110 | await runPhp( 111 | [COMPOSER_BIN, 'run', 'vercel'], 112 | { 113 | stdio: 'inherit', 114 | cwd: workPath 115 | } 116 | ); 117 | 118 | console.log('🐘 Running composer scripts [DONE]'); 119 | } 120 | } 121 | 122 | export async function ensureLocalPhp(): Promise { 123 | try { 124 | await spawnAsync('which', ['php', 'php-cgi'], { stdio: 'pipe' }); 125 | return true; 126 | } catch (e) { 127 | return false; 128 | } 129 | } 130 | 131 | export async function readRuntimeFile(file: File): Promise { 132 | const blob = await FileBlob.fromStream({ 133 | stream: file.toStream(), 134 | }); 135 | 136 | return blob.data.toString(); 137 | } 138 | 139 | // ***************************************************************************** 140 | // PRIVATE API ***************************************************************** 141 | // ***************************************************************************** 142 | 143 | async function runPhp(args: ReadonlyArray, opts: SpawnOptions = {}) { 144 | try { 145 | await spawnAsync('php', args, 146 | { 147 | ...opts, 148 | env: { 149 | ...process.env, 150 | ...(opts.env || {}), 151 | COMPOSER_HOME: '/tmp', 152 | PATH: `${PHP_BIN_DIR}:${process.env.PATH}`, 153 | PHP_INI_EXTENSION_DIR: PHP_MODULES_DIR, 154 | PHP_INI_SCAN_DIR: `:${path.resolve(__dirname, '../conf')}`, 155 | LD_LIBRARY_PATH: `${PHP_LIB_DIR}:/usr/lib64:/lib64:${process.env.LD_LIBRARY_PATH}` 156 | } 157 | } 158 | ); 159 | } catch (e) { 160 | console.error(e); 161 | process.exit(1); 162 | } 163 | } 164 | 165 | function spawnAsync(command: string, args: ReadonlyArray, opts: SpawnOptions = {}): Promise { 166 | return new Promise((resolve, reject) => { 167 | const child = spawn(command, args, { 168 | stdio: "ignore", 169 | ...opts 170 | }); 171 | 172 | child.on('error', reject); 173 | child.on('exit', (code, signal) => { 174 | if (code === 0) { 175 | resolve(); 176 | } else { 177 | reject(new Error(`Exited with ${code || signal}`)); 178 | } 179 | }); 180 | }) 181 | } 182 | -------------------------------------------------------------------------------- /test/examples/00-php/.gitignore: -------------------------------------------------------------------------------- 1 | # Vercel 2 | .vercel 3 | -------------------------------------------------------------------------------- /test/examples/00-php/api/api/index.php: -------------------------------------------------------------------------------- 1 | 1, 'name' => 'f3l1x'], 5 | ['id' => 2, 'name' => 'chemix'], 6 | ['id' => 3, 'name' => 'dg'], 7 | ['id' => 4, 'name' => 'milo'], 8 | ['id' => 5, 'name' => 'matej21'], 9 | ['id' => 6, 'name' => 'merxes'], 10 | ]; 11 | 12 | header('Content-Type: application/json'); 13 | 14 | echo json_encode($data); 15 | -------------------------------------------------------------------------------- /test/examples/00-php/api/ext/ds.php: -------------------------------------------------------------------------------- 1 | 1, "b" => 2, "c" => 3]); 3 | $map->apply(function($key, $value) { return $value * 2; }); 4 | 5 | print_r($map); 6 | ?> -------------------------------------------------------------------------------- /test/examples/00-php/api/ext/gd.php: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/examples/00-php/api/ext/index.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/examples/00-php/api/ext/phalcon.php: -------------------------------------------------------------------------------- 1 | get( 8 | "/", 9 | function () { 10 | echo "

Welcome!

"; 11 | } 12 | ); 13 | 14 | $app->get( 15 | "/say/hello/{name}", 16 | function ($name) use ($app) { 17 | echo "

Hello! $name

"; 18 | echo "Your IP Address is ", $app->request->getClientAddress(); 19 | } 20 | ); 21 | 22 | $app->post( 23 | "/store/something", 24 | function () use ($app) { 25 | $name = $app->request->getPost("name"); 26 | echo "

Hello! $name

"; 27 | } 28 | ); 29 | 30 | $app->notFound( 31 | function () use ($app) { 32 | $app->response->setStatusCode(404, "Not Found"); 33 | $app->response->sendHeaders(); 34 | echo "This is crazy, but this page was not found!"; 35 | } 36 | ); 37 | 38 | $app->handle(); 39 | -------------------------------------------------------------------------------- /test/examples/00-php/api/hello.php: -------------------------------------------------------------------------------- 1 | php.ini 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | $group) { 15 | echo ""; 16 | echo sprintf('', $name); 17 | echo sprintf('', $group['global_value']); 18 | echo sprintf('', $group['local_value']); 19 | echo sprintf('', $group['access']); 20 | echo ""; 21 | } 22 | ?> 23 | 24 |
optionglobal_valuelocal_valueaccess
%s%s%s%s
25 | -------------------------------------------------------------------------------- /test/examples/00-php/api/libs.php: -------------------------------------------------------------------------------- 1 | "; 9 | } else { 10 | $files = scandir($path); 11 | echo "Scan folder: $path
"; 12 | array_map(function($file) { 13 | echo "- $file
"; 14 | }, $files); 15 | } 16 | echo "
"; 17 | } 18 | -------------------------------------------------------------------------------- /test/examples/00-php/api/test.php: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /test/examples/02-extensions/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [{ "src": "index.php", "use": "vercel-php@0.7.3" }] 4 | } 5 | -------------------------------------------------------------------------------- /test/examples/03-env-vars/env/index.php: -------------------------------------------------------------------------------- 1 | =5.3.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Log\\": "Psr/Log/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "http://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for logging libraries", 48 | "homepage": "https://github.com/php-fig/log", 49 | "keywords": [ 50 | "log", 51 | "psr", 52 | "psr-3" 53 | ], 54 | "time": "2018-11-20T15:27:04+00:00" 55 | } 56 | ], 57 | "packages-dev": [], 58 | "aliases": [], 59 | "minimum-stability": "stable", 60 | "stability-flags": [], 61 | "prefer-stable": false, 62 | "prefer-lowest": false, 63 | "platform": [], 64 | "platform-dev": [] 65 | } 66 | -------------------------------------------------------------------------------- /test/examples/10-composer-builds/index.php: -------------------------------------------------------------------------------- 1 | =5.3.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Log\\": "Psr/Log/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "http://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for logging libraries", 48 | "homepage": "https://github.com/php-fig/log", 49 | "keywords": [ 50 | "log", 51 | "psr", 52 | "psr-3" 53 | ], 54 | "time": "2018-11-20T15:27:04+00:00" 55 | } 56 | ], 57 | "packages-dev": [], 58 | "aliases": [], 59 | "minimum-stability": "stable", 60 | "stability-flags": [], 61 | "prefer-stable": false, 62 | "prefer-lowest": false, 63 | "platform": [], 64 | "platform-dev": [] 65 | } 66 | -------------------------------------------------------------------------------- /test/examples/11-composer-env/index.php: -------------------------------------------------------------------------------- 1 | { 4 | const mockLog = console.log = jest.fn(); 5 | 6 | jest.spyOn(process, 'exit').mockImplementation((code) => { 7 | expect(code).toBe(255); 8 | expect(mockLog).toHaveBeenCalledTimes(1); 9 | }); 10 | 11 | await builder.build({ 12 | files: [], 13 | entrypoint: 'test.php', 14 | workPath: __dirname, 15 | config: {}, 16 | meta: { isDev: true }, 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/spec/index.js: -------------------------------------------------------------------------------- 1 | const builder = require('./../../dist/index'); 2 | 3 | test('creates simple lambda', async () => { 4 | await builder.build({ 5 | files: [], 6 | entrypoint: 'test.php', 7 | workPath: __dirname, 8 | config: {}, 9 | meta: {}, 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/spec/launchers/cgi.js: -------------------------------------------------------------------------------- 1 | const cgi = require('./../../../dist/launchers/cgi'); 2 | 3 | test('create CGI request', () => { 4 | const request = { 5 | entrypoint: "index.php", 6 | path: "/index.php", 7 | host: "https://vercel.com", 8 | method: "GET", 9 | headers: {} 10 | }; 11 | process.env.CUSTOM_VALUE = "custom-value"; 12 | const { env } = cgi.createCGIReq(request); 13 | 14 | expect(env).toHaveProperty("SERVER_ROOT", "/user"); 15 | expect(env).toHaveProperty("DOCUMENT_ROOT", "/user"); 16 | expect(env).toHaveProperty("SERVER_NAME", request.host); 17 | expect(env).toHaveProperty("SERVER_PORT", 443); 18 | expect(env).toHaveProperty("HTTPS", 'On'); 19 | expect(env).toHaveProperty("REDIRECT_STATUS", 200); 20 | expect(env).toHaveProperty("SCRIPT_NAME", request.entrypoint); 21 | expect(env).toHaveProperty("REQUEST_URI", request.path); 22 | expect(env).toHaveProperty("SCRIPT_FILENAME", request.entrypoint); 23 | expect(env).toHaveProperty("PATH_TRANSLATED", request.entrypoint); 24 | expect(env).toHaveProperty("REQUEST_METHOD", request.method); 25 | expect(env).toHaveProperty("QUERY_STRING", ''); 26 | expect(env).toHaveProperty("GATEWAY_INTERFACE", 'CGI/1.1'); 27 | expect(env).toHaveProperty("SERVER_PROTOCOL", 'HTTP/1.1'); 28 | expect(env).toHaveProperty("SERVER_SOFTWARE", 'Vercel PHP'); 29 | expect(env).toHaveProperty("PATH", process.env.PATH); 30 | expect(env).toHaveProperty("LD_LIBRARY_PATH", process.env.LD_LIBRARY_PATH); 31 | expect(env).toHaveProperty("CUSTOM_VALUE", process.env.CUSTOM_VALUE); 32 | }); 33 | -------------------------------------------------------------------------------- /test/spec/path.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | test('relative path', () => { 4 | const rootdir = '/var/task/user'; 5 | const request = '/var/task/user/api/users.php'; 6 | const file = path.relative(rootdir, request); 7 | 8 | expect(file).toBe('api/users.php'); 9 | }); 10 | -------------------------------------------------------------------------------- /test/spec/url.js: -------------------------------------------------------------------------------- 1 | const url = require('url'); 2 | 3 | test('url.parse search & query are string', () => { 4 | const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo'); 5 | expect(search).toBe('?foo=bar&foo2=baz'); 6 | expect(query).toBe('foo=bar&foo2=baz'); 7 | }); 8 | 9 | test('url.parse search string, query object', () => { 10 | const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo', true); 11 | expect(search).toBe('?foo=bar&foo2=baz'); 12 | expect(query).toMatchObject({ foo: 'bar', 'foo2': 'baz' }); 13 | }); 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "esModuleInterop": true, 5 | "resolveJsonModule": true, 6 | "moduleResolution": "node", 7 | "allowSyntheticDefaultImports": true, 8 | "lib": [ 9 | "ES2019" 10 | ], 11 | "target": "ES2019", 12 | "module": "CommonJS", 13 | "outDir": "dist", 14 | "sourceMap": false, 15 | "declaration": true, 16 | "noImplicitAny": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "forceConsistentCasingInFileNames": true, 20 | "skipLibCheck": true, 21 | "typeRoots": [ 22 | "./node_modules/@types" 23 | ] 24 | }, 25 | "include": [ 26 | "src/**/*.ts" 27 | ], 28 | "exclude": [ 29 | "errors", 30 | "dist", 31 | "node_modules", 32 | "test" 33 | ] 34 | } 35 | --------------------------------------------------------------------------------