Repository: juicyfx/now-php Branch: master Commit: 2c2f4f8561bd Files: 118 Total size: 62.6 KB Directory structure: gitextract_vi8hg7za/ ├── .editorconfig ├── .github/ │ ├── .kodiak.toml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug.md │ │ ├── Feature.md │ │ └── Question.md │ ├── dependabot.yml │ └── workflows/ │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── CLAUDE.md ├── LICENSE ├── Makefile ├── README.md ├── conf/ │ └── build.ini ├── errors/ │ └── now-dev-no-local-php.md ├── jest.config.js ├── 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 │ │ │ ├── index.php │ │ │ └── vercel.json │ │ ├── 11-composer-env/ │ │ │ ├── .gitignore │ │ │ ├── .vercelignore │ │ │ ├── composer-test.json │ │ │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # EditorConfig is awesome: http://EditorConfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{html}] indent_style = tab indent_size = tab tab_width = 4 [*.{js,ts,json,yml,yaml,md}] indent_style = space indent_size = 2 ================================================ FILE: .github/.kodiak.toml ================================================ version = 1 [merge] automerge_label = "automerge" blacklist_title_regex = "^WIP.*" blacklist_labels = ["WIP"] method = "rebase" delete_branch_on_merge = true notify_on_conflict = true optimistic_updates = false ================================================ FILE: .github/ISSUE_TEMPLATE/Bug.md ================================================ --- name: Bug report 🐛 about: Something is not working as expected! --- # Bug report - Version: x.y.z - URL: Yes (*.now.sh) / No - Repository: Yes / No ## Description ================================================ FILE: .github/ISSUE_TEMPLATE/Feature.md ================================================ --- name: Feature request 🚀 about: I would appreciate new feature or something! --- # Feature Request ================================================ FILE: .github/ISSUE_TEMPLATE/Question.md ================================================ --- name: Question ❓ about: Ask about anything! --- # Question ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily time: '11:00' open-pull-requests-limit: 10 - package-ecosystem: npm directory: "/packages/php" schedule: interval: daily time: '11:00' open-pull-requests-limit: 10 - package-ecosystem: npm directory: "/packages/caddy" schedule: interval: daily time: '11:00' open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/main.yml ================================================ name: Main workflow on: [push, pull_request] jobs: run: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Setup Node uses: actions/setup-node@v3 with: node-version: '22.x' - name: Dependencies run: make install - name: Build run: make build - name: Tests run: make test ================================================ FILE: .gitignore ================================================ # Node /node_modules /package-lock.json # App /dist # Vercel .vercel ================================================ FILE: CHANGELOG.md ================================================ # Changelog ### [0.9.0] - 2026-01-15 - PHP 8.5 ### [0.8.0] - 2026-01-15 - PHP 8.4 ### [0.7.4] - 2025-07-17 - Upgrade PHP 7.4-8-3 runtimes for Node 22 ### [0.7.3] - 2024-10-19 - Upgrade PHP 8.3 runtime (fixes for curl, opcache) ### [0.7.2] - 2024-09-30 - Upgrade PHP 8.3 runtime ### [0.7.1] - 2024-04-16 - Fix autodetect runtime ### [0.6.2] - 2024-04-16 - Fix autodetect runtime ### [0.5.5] - 2024-04-16 - Fix autodetect runtime ### [0.4.4] - 2024-04-16 - Fix autodetect runtime ### [0.3.6] - 2024-04-16 - Fix autodetect runtime ### [0.7.0] - 2024-02-22 - PHP 8.3 - Use `@libphp/amazon-linux-2-v83: latest` ### [0.6.1] - 2024-01-24 - Update LD_LIBRARY_PATH ### [0.5.4] - 2024-01-24 - Update LD_LIBRARY_PATH ### [0.4.3] - 2024-01-24 - Update LD_LIBRARY_PATH ### [0.3.5] - 2024-01-24 - Update LD_LIBRARY_PATH ### [0.6.0] - 2023-03-27 - PHP 8.2 - Use `@libphp/amazon-linux-2-v82: latest` ### [0.5.3] - 2023-03-27 - Bump minimum node version from 14.x to 18.x - Upgrade dependencies ### [0.5.2] - 2022-08-10 - Bump minimum node version from 12.x to 14.x ### [0.5.1] - 2022-05-05 - Ignore .vercel folder during deployment ### [0.5.0] - 2022-04-09 - PHP 8.1 - Added extensions: geoip, zlib, zip - Removed extensions: psr - Use `@libphp/amazon-linux-2-v81: latest` ### [0.4.0] - 2021-01-02 - PHP 8.0 - Use `@libphp/amazon-linux-2-v80: latest` ### [0.3.2] - 2021-01-02 - Typos - More hints in FAQ - Fix `excludeFiles` option - Install PHP extensions mongodb - Use `@libphp/amazon-linux-2-v74: latest` ### [0.3.1] - 2020-07-04 - Install PHP extensions redis, msgpack, igbinary - Use `@libphp/amazon-linux-2-v74: latest` ### [0.3.0] - 2020-06-29 - Allow to execute composer script called `vercel` ```json { "scripts": { "vercel": [ "@php -v", "npm -v" ] } } ``` - Drop support of `config['php.ini']` use `api/php.ini` file instead - Support excludeFiles (default `['node_modules/**', 'now.json', '.nowignore']`) ```json { "functions": { "api/**/*.php": { "runtime": "vercel-php@0.3.0", "excludeFiles": ["node_modules", "somedir", "foo/bar"], } } ``` - Restructure test folder (merge fixtures + my examples) ### [0.2.0] - 2020-06-26 - Allow to override `php.ini` ```sh project ├── api │ ├── index.php │ └── php.ini └── now.json ``` - Extensive update of docs - Introduce FAQ questions - Move caddy package to [juicyfx/juicy](https://github.com/juicyfx/juicy) - Simplify repository structure ### [0.1.0] - 2020-06-20 - Rename repository from now-php to **vercel-php** - Rename NPM package from now-php to **vercel-php** - Upgrade PHP to 7.4.7 and recompile PHP extensions - Improve readme - Separate PHP libs to solo repository [juicyfx/libphp](https://github.com/juicyfx/libphp) (bigger plans) - Use [php.vercel.app](https://php.vercel.app) domain for official showtime - Use [phpshow.vercel.app](https://phpshow.vercel.app) domain for runtime showcase ### [0.0.9] - 2020-03-28 - Use PHP 7.4 for installing Composer dependencies - Upgrade PHP 7.4 and recompile PHP extensions ### [0.0.9] - 2020-01-16 - Use PHP 7.3 for installing Composer dependencies - Separate [examples](https://github.com/juicyfx/vercel-examples) to solo repository - Extensions - Disabled ssh2 - Added psr - Rebuild phalcon, swoole ### [0.0.8] - 2020-01-07 - Runtime v3 - Upgrade to PHP 7.4.x - Node 8.x reached EOL on AWS - Used Amazon Linux 2 - CGI launcher inherits process.env [#38] - Drop Circle CI - Rebuild all PHP libs ### [0.0.7] - 2019-11-08 - Rename builder to runtime - Runtime v3 **Migration** ```json { "version": 2, "builds": [ { "src": "index.php", "use": "now-php" } ] } ``` ➡️ ```json { "functions": { "api/*.php": { "runtime": "now-php@0.0.7" } }, // Optionally provide routes "routes": [ { "src": "/(.*)", "dest": "/api/index.php" } ] } ``` ### [0.0.6] - 2019-11-07 - Change builds to functions ### [0.0.5] - 2019-09-30 - Added Lumen example - Bugfix deploying PHP files in folders under different names then index.php ### [0.0.4] - 2019-09-30 - Implement intermediate caching (vendor, composer.lock, yarn.locak, package-lock.json, node_modules) - Rewrite PHP built-in server document root ### [0.0.3] - 2019-09-04 - Bugfix passing query parameters and accessing $_GET ### [0.0.2] - 2019-08-23 - Bump now-php@latest ### [0.0.1-canary.39] - 2019-08-23 - Allow overriding php.ini - Bugfix resolving PHP bin - Bugfix deploying php files in subfolders ### [0.0.2-canary.2] - 2019-08-16 - Compile PHP 7.3.8 ### [0.0.1-canary.5] - 2019-08-16 - First working copy of caddy server ### [0.0.1-canary.30] - 2019-08-16 - New exported method `getPhpLibFiles` - Repair tests ### [0.0.1-canary.18] - 2019-08-02 - Bump now-php@latest ### [0.0.1-canary.18] - 2019-08-02 - Working on change response from string to Buffer - Updated homepage ### [0.0.1-canary.17] - 2019-08-02 - Working on change response from string to Buffer ### [0.0.1-canary.15] - 2019-08-02 - CGI: REQUEST_URI contains only path, not host + path - CGI: QUERY_STRING contains string without leading ? ### [0.0.1-canary.14] - 2019-07-29 - Tests: more tests ### [0.0.1-canary.13] - 2019-07-29 - Tests: take tests from official old builder ### [0.0.1-canary.12] - 2019-07-28 - Rewritten to TypeScript ### [0.0.1-canary.11] - 2019-07-28 - Working on `now-dev` ### [0.0.1-canary.8] - 2019-07-27 - First working `now-php` builder ### [0.0.1-canary.7] - 2019-07-27 - Working on `now` with `now-php` ### [0.0.1-canary.0] - 2019-07-27 - History begins ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview **vercel-php** is a PHP runtime for the Vercel platform enabling serverless PHP applications. It bundles PHP 8.3 with common extensions and supports multiple execution modes (built-in server, CGI, CLI). ## Commands ```bash make install # Install dependencies (npm install) make build # Compile TypeScript to dist/ (npm run build) make build-watch # Watch mode compilation (npm run watch) make test # Run Jest test suite (npm run test) make test-watch # Run tests in watch mode make publish # Publish to npm (latest tag) make canary # Publish to npm (canary tag) ``` ## Architecture ### Vercel BuildV3 Runtime The package implements Vercel's BuildV3 specification, exporting: - `version = 3` - API version - `build()` - Main builder function - `prepareCache()` - Cache preparation - `shouldServe()` - File serving decisions ### Build Flow (src/index.ts) 1. Download user files and PHP runtime (`@libphp/amazon-linux-2-v83`) 2. Run `composer install` if `composer.json` exists 3. Execute `composer run vercel` script if defined 4. Merge user `api/php.ini` with runtime defaults 5. Package everything into AWS Lambda function ### Runtime Execution Modes (src/launchers/) - **builtin.ts** - PHP's built-in server (`php -S 0.0.0.0:3000`), proxied via Node.js. Default mode. - **cgi.ts** - Spawns `php-cgi` per request with CGI environment variables. Stateless. - **cli.ts** - Direct PHP CLI execution for simple scripts. ### Key Source Files - `src/index.ts` - Build entry point implementing BuildV3 - `src/utils.ts` - Build utilities (Composer, PHP config, file collection) - `src/launchers/helpers.ts` - Request/response transformation between Vercel events and PHP ### Type Definitions (src/types.d.ts) Key types: `UserFiles`, `RuntimeFiles`, `Event`, `InvokedEvent`, `AwsRequest`, `AwsResponse`, `PhpInput`, `PhpOutput`, `CgiInput` ## Code Conventions - Logging uses 🐘 emoji prefix for console output - Exit codes: 253-255 for critical child process errors, 1 for general errors - User files are prefixed with `/user/` in Lambda task root - TypeScript strict mode with `noImplicitAny`, `noUnusedLocals`, `noUnusedParameters` - 2-space indentation for JS/TS/JSON/YAML/MD files ## Testing Tests are in `test/spec/` with example projects in `test/examples/`. Jest timeout is 10 seconds. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Milan Felix Sulc (f3l1x) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ .PHONY: install build test publish canary install: npm install build: npm run build build-watch: npm run watch test: npm run test test-watch: npm run test-watch publish: rm -rf ./dist npm publish --access public --tag latest canary: rm -rf ./dist npm publish --access public --tag canary ================================================ FILE: README.md ================================================

PHP Runtime for Vercel

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

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

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

----- ## 😎 Getting Started 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. Most simple example project is this one, using following project structure. ```sh project ├── api │ └── index.php └── vercel.json ``` First file `api/index.php` is entrypoint of our application. It should be placed in **api** folder, it's very standard location for Vercel. ```php ## 🤗 Features - **Architecture**: PHP development server (🚀 fast enough) - **PHP version**: 8.5 (https://example-php-8-5.vercel.app) - **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 - **Speed**: cold ~250ms / warm ~5ms - **Memory**: ~90mb - **Frameworks**: Nette, Symfony, Lumen, Slim, Phalcon - **Node.js**: 22.x > List of all installable extensions is on this page https://blog.remirepo.net/pages/PECL-extensions-RPM-status. ## 💯 Versions - `vercel-php@0.9.0` - Node autodetect / PHP 8.5.x (https://example-php-8-5.vercel.app) - `vercel-php@0.8.0` - Node autodetect / PHP 8.4.x (https://example-php-8-4.vercel.app) - `vercel-php@0.7.4` - Node autodetect / PHP 8.3.x (https://example-php-8-3.vercel.app) - `vercel-php@0.6.2` - Node autodetect / PHP 8.2.x (https://example-php-8-2.vercel.app) - `vercel-php@0.5.5` - Node autodetect / PHP 8.1.x (https://example-php-8-1.vercel.app) - `vercel-php@0.4.5` - Node autodetect / PHP 8.0.x (https://example-php-8-0.vercel.app) - `vercel-php@0.3.8` - Node autodetect / PHP 7.4.x (https://example-php-7-4.vercel.app) ## ⚙️ Usage 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). You should define `functions` property in `vercel.json` and list PHP files directly or using wildcard (*). If you need to route everything to index, use `routes` property. ```json { "functions": { "api/*.php": { "runtime": "vercel-php@0.9.0" } }, "routes": [ { "src": "/(.*)", "dest": "/api/index.php" } ] } ``` Do you have more questions (❓)? Let's move to [FAQ](#%EF%B8%8F-faq). ## 👨‍💻 `vercel dev` For running `vercel dev` properly, you need to have PHP installed on your computer, [learn more](errors/now-dev-no-local-php.md). But it's PHP and as you know PHP has built-in development server. It works out of box. ``` php -S localhost:8000 api/index.php ``` ## 👀 Demo - official - https://php.vercel.app/ - phpinfo - https://phpshow.vercel.app/ - extensions - https://phpshow.vercel.app/ext/ - ini - https://phpshow.vercel.app/ini/ - JSON API - https://phpshow.vercel.app/api/users.php - test - https://phpshow.vercel.app/test.php ![PHP](https://api.microlink.io?url=https://phpshow.vercel.app&screenshot&embed=screenshot.url) ## 🎯Examples - [PHP - fast & simple](https://github.com/juicyfx/vercel-examples/tree/master/php/) - [Composer - install dependencies](https://github.com/juicyfx/vercel-examples/tree/master/php-composer/) - [Framework - Laravel](https://github.com/juicyfx/vercel-examples/blob/master/php-laravel) - [Framework - Lumen](https://github.com/juicyfx/vercel-examples/blob/master/php-lumen) - [Framework - Nette](https://github.com/juicyfx/vercel-examples/blob/master/php-nette-tracy) - [Framework - Phalcon](https://github.com/juicyfx/vercel-examples/blob/master/php-phalcon) - [Framework - Slim](https://github.com/juicyfx/vercel-examples/blob/master/php-slim) - [Framework - Symfony - Microservice](https://github.com/juicyfx/vercel-examples/blob/master/php-symfony-microservice) Browse [more examples](https://github.com/juicyfx/vercel-examples). 👀 ## 📜 Resources - [2019/10/23 - Code Examples](https://github.com/trainit/2019-10-hubbr-zeit) - [2019/10/19 - ZEIT - Deploy Serverless Microservices Right Now](https://slides.com/f3l1x/2019-10-19-zeit-deploy-serverless-microservices-right-now-vol2) - [2019/08/23 - Code Examples](https://github.com/trainit/2019-08-serverless-zeit-now) - [2019/07/07 - Bleeding Edge PHP on ZEIT Now](https://dev.to/nx1/bleeding-edge-php-on-zeit-now-565g) - [2019/06/06 - Code Examples](https://github.com/trainit/2019-06-zeit-now) - [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)) ## 🚧 Roadmap See [roadmap issue](https://github.com/juicyfx/vercel-php/issues/3). Help wanted. ## ⁉️ FAQ
1. How to use more then one endpoint (index.php)? ```sh project ├── api │ ├── index.php │ ├── users.php │ └── books.php └── vercel.json ``` ``` { "functions": { "api/*.php": { "runtime": "vercel-php@0.9.0" }, // Can be list also directly "api/index.php": { "runtime": "vercel-php@0.9.0" }, "api/users.php": { "runtime": "vercel-php@0.9.0" }, "api/books.php": { "runtime": "vercel-php@0.9.0" } } } ```
2. How to route everything to index? ```json { "functions": { "api/index.php": { "runtime": "vercel-php@0.9.0" } }, "routes": [ { "src": "/(.*)", "dest": "/api/index.php" } ] } ```
3. How to update memory limit? Additional function properties are `memory`, `maxDuration`. Learn more about [functions](https://vercel.com/docs/configuration#project/functions). ```json { "functions": { "api/*.php": { "runtime": "vercel-php@0.9.0", "memory": 3008, "maxDuration": 60 } } } ```
4. How to use it with Composer? Yes, [Composer](https://getcomposer.org/) is fully supported. ```sh project ├── api │ └── index.php ├── composer.json └── vercel.json ``` ```json { "functions": { "api/*.php": { "runtime": "vercel-php@0.9.0" } } } ``` ```json { "require": { "php": "^8.1", "tracy/tracy": "^2.0" } } ``` It's also good thing to create `.vercelignore` file and put `/vendor` folder to this file. It will not upload `/vendor` folder to Vercel platform.
5. How to override php.ini / php configuration ? Yes, you can override php configuration. Take a look at [default configuration](https://phpshow.vercel.app/) at first. Create a new file `api/php.ini` and place there your configuration. Don't worry, this particulary file will be removed during building phase on Vercel. ```sh project ├── api │ ├── index.php │ └── php.ini └── vercel.json ``` ```json { "functions": { "api/*.php": { "runtime": "vercel-php@0.9.0" } } } ``` ```json # Disable some functions disable_functions = "exec, system" # Update memory limit memory_limit=1024M ```
6. How to exclude some files or folders ? Runtimes support excluding some files or folders, [take a look at doc](https://vercel.com/docs/configuration?query=excludeFiles#project/functions). ```json { "functions": { "api/**/*.php": { "runtime": "vercel-php@0.9.0", "excludeFiles": "{foo/**,bar/config/*.yaml}", } } ``` If you want to exclude files before uploading them to Vercel, use `.vercelignore` file.
7. How to call composer script(s) ? Calling composer scripts during build phase on Vercel is supported via script named `vercel`. You can easilly call php, npm or node. ```json { "require": { ... }, "require-dev": { ... }, "scripts": { "vercel": [ "@php -v", "npm -v" ] } } ``` 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).
8. How to include some files of folders? 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. All files in root folder are uploaded to Vercel, use `.vercelignore` to exclude them before upload.
9. How to develop locally? I think the best way at this moment is use [PHP Development Server](https://www.php.net/manual/en/features.commandline.webserver.php). ``` php -S localhost:8000 api/index.php ```
10. What Node.js runtime is supported? Use 22.x.
## 👨🏻‍💻CHANGELOG Show me [CHANGELOG](./CHANGELOG.md) ## 🧙Contribution 1. Clone this repository. - `git clone git@github.com:juicyfx/vercel-php.git` 2. Install NPM dependencies - `make install` 3. Make your changes 4. Run TypeScript compiler - `make build` 5. Run tests - `make test` 6. Create a PR ## 📝 License Copyright © 2019 [f3l1x](https://github.com/f3l1x). This project is [MIT](LICENSE) licensed. ================================================ FILE: conf/build.ini ================================================ ; Override for build phase on Vercel extension_dir = "${PHP_INI_EXTENSION_DIR}" ================================================ FILE: errors/now-dev-no-local-php.md ================================================ # It looks like you don't have PHP on your machine. **Why This Error Occurred** You ran `now dev` on a machine where PHP is not installed. For the time being, this runtime requires a local PHP installation to run the runtime locally. **Possible Ways to Fix It** 1. Install PHP to your computer **OSX** ``` brew install php@7.4 ``` **Ubuntu** ``` apt-get -y install apt-transport-https lsb-release ca-certificates wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list' apt-get update apt-get install php7.4-cli php7.4-cgi php7.4-json php7.4-curl php7.4-mbstring ``` **Fedora** ``` yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm yum install https://rpms.remirepo.net/enterprise/remi-release-7.rpm yum install yum-utils yum-config-manager --enable remi-php74 yum update yum install php74-cli php74-cgi php74-json php74-curl php74-mbstring ``` 2. Start PHP built-in Development Server ```sh php -S localhost:8000 api/index.php ``` **Check that php is in the path** If you do have installed PHP but still get this error, check that PHP executable is added to the PATH environment variable. ================================================ FILE: jest.config.js ================================================ module.exports = { rootDir: ".", verbose: true, testEnvironment: "node", testMatch: [ "**/test/spec/**/*.js", ], testPathIgnorePatterns: [ "/errors/", "/dist/", "/node_modules/", ], testTimeout: 10000 } ================================================ FILE: package.json ================================================ { "name": "vercel-php", "description": "Vercel PHP runtime", "version": "0.9.0", "license": "MIT", "main": "./dist/index.js", "homepage": "https://github.com/vercel-community/php", "repository": { "type": "git", "url": "https://github.com/vercel-community/php.git" }, "keywords": [ "vercel", "php", "builder", "runtime", "serverless", "deployment" ], "scripts": { "watch": "tsc --watch", "build": "tsc", "test": "jest --silent", "test-watch": "jest --watch", "prepublishOnly": "tsc" }, "files": [ "dist", "conf" ], "dependencies": { "@libphp/almalinux-9-v85": ">=0.0.2" }, "devDependencies": { "@types/glob": "^9.0.0", "@types/node": "^22.0.0", "@vercel/build-utils": "^13.2.9", "jest": "^30.2.0", "typescript": "^5.9.3" } } ================================================ FILE: src/index.ts ================================================ import path from 'path'; import { rename, shouldServe, glob, download, Lambda, BuildV3, PrepareCache, getNodeVersion } from '@vercel/build-utils'; import { getPhpFiles, getLauncherFiles, runComposerInstall, runComposerScripts, readRuntimeFile, modifyPhpIni, } from './utils'; const COMPOSER_FILE = process.env.COMPOSER || 'composer.json'; // ########################### // EXPORTS // ########################### export const version = 3; export const build: BuildV3 = async ({ files, entrypoint, workPath, config = {}, meta = {}, }) => { // Check if now dev mode is used if (meta.isDev) { console.log(` 🐘 vercel dev is not supported right now. Please use PHP built-in development server. php -S localhost:8000 api/index.php `); process.exit(255); } console.log('🐘 Downloading user files'); // Collect user provided files const userFiles: RuntimeFiles = await download(files, workPath, meta); console.log('🐘 Downloading PHP runtime files'); // Collect runtime files containing PHP bins and libs const runtimeFiles: RuntimeFiles = { // Append PHP files (bins + shared object) ...await getPhpFiles(), // Append launcher files (builtin server, common helpers) ...getLauncherFiles(), }; // If composer.json is provided try to // - install deps // - run composer scripts if (userFiles[COMPOSER_FILE]) { // Install dependencies (vendor is collected bellow, see harvestedFiles) await runComposerInstall(workPath); // Run composer scripts (created files are collected bellow, , see harvestedFiles) await runComposerScripts(userFiles[COMPOSER_FILE], workPath); } // Append PHP directives into php.ini if (userFiles['api/php.ini']) { const phpini = await modifyPhpIni(userFiles, runtimeFiles); if (phpini) { runtimeFiles['php/php.ini'] = phpini; } } // Collect user files, files creating during build (composer vendor) // and other files and prefix them with "user" (/var/task/user folder). const harverstedFiles = rename( await glob('**', { cwd: workPath, ignore: [ '.vercel/**', ...(config?.excludeFiles ? Array.isArray(config.excludeFiles) ? config.excludeFiles : [config.excludeFiles] : [ 'node_modules/**', 'now.json', '.nowignore', 'vercel.json', '.vercelignore', ]), ], }), name => path.join('user', name) ); // Show some debug notes during build if (process.env.NOW_PHP_DEBUG === '1') { console.log('🐘 Entrypoint:', entrypoint); console.log('🐘 Config:', config); console.log('🐘 Work path:', workPath); console.log('🐘 Meta:', meta); console.log('🐘 User files:', Object.keys(harverstedFiles)); console.log('🐘 Runtime files:', Object.keys(runtimeFiles)); console.log('🐘 PHP: php.ini', await readRuntimeFile(runtimeFiles['php/php.ini'])); } console.log('🐘 Creating lambda'); const nodeVersion = await getNodeVersion(workPath); const lambda = new Lambda({ files: { // Located at /var/task/user ...harverstedFiles, // Located at /var/task/php (php bins + ini + modules) // Located at /var/task/lib (shared libs) ...runtimeFiles }, handler: 'launcher.launcher', runtime: nodeVersion.runtime, environment: { NOW_ENTRYPOINT: entrypoint, NOW_PHP_DEV: meta.isDev ? '1' : '0' }, }); return { output: lambda }; }; export const prepareCache: PrepareCache = async ({ workPath }) => { return { // Composer ...(await glob('vendor/**', workPath)), ...(await glob('composer.lock', workPath)), // NPM ...(await glob('node_modules/**', workPath)), ...(await glob('package-lock.json', workPath)), ...(await glob('yarn.lock', workPath)), ...(await glob('pnpm-lock.yaml', workPath)), // Bun ...(await glob('bun.lock', workPath)), /* in case still used */ ...(await glob('bun.lockb', workPath)), }; }; export { shouldServe }; ================================================ FILE: src/launchers/builtin.ts ================================================ import http from 'http'; import { spawn, ChildProcess, SpawnOptions } from 'child_process'; import net from 'net'; import { getPhpDir, getUserDir, normalizeEvent, transformFromAwsRequest, transformToAwsResponse, isDev } from './helpers'; import { join as pathJoin } from 'path'; let server: ChildProcess; async function startServer(entrypoint: string): Promise { // Resolve document root and router const router = entrypoint; const docroot = pathJoin(getUserDir(), process.env.VERCEL_PHP_DOCROOT ?? ''); console.log(`🐘 Spawning: PHP Built-In Server at ${docroot} (document root) and ${router} (router)`); // php spawn options const options: SpawnOptions = { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, LD_LIBRARY_PATH: `/var/task/lib:${process.env.LD_LIBRARY_PATH}` } }; // now vs now-dev if (!isDev()) { options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; options.cwd = getPhpDir(); } else { options.cwd = getUserDir(); } // We need to start PHP built-in server with following setup: // php -c php.ini -S ip:port -t /var/task/user /var/task/user/foo/bar.php // // Path to document root lambda task folder with user prefix, because we move all // user files to this folder. // // Path to router is absolute path, because CWD is different. // server = spawn( 'php', ['-c', 'php.ini', '-S', '0.0.0.0:3000', '-t', docroot, router], options, ); server.stdout?.on('data', data => { console.log(`🐘STDOUT: ${data.toString()}`); }); server.stderr?.on('data', data => { console.error(`🐘STDERR: ${data.toString()}`); }); server.on('close', function (code, signal) { console.log(`🐘 PHP Built-In Server process closed code ${code} and signal ${signal}`); }); server.on('error', function (err) { console.error(`🐘 PHP Built-In Server process errored ${err}`); }); await whenPortOpens(3000, 500); process.on('exit', () => { server.kill(); }) return server; } async function query({ entrypoint, uri, path, headers, method, body }: PhpInput): Promise { if (!server) { await startServer(entrypoint); } return new Promise(resolve => { const options = { hostname: 'localhost', port: 3000, path, method, headers, }; console.log(`🐘 Accessing ${uri}`); console.log(`🐘 Querying ${path}`); const req = http.request(options, (res) => { const chunks: Uint8Array[] = []; res.on('data', (data) => { chunks.push(data); }); res.on('end', () => { resolve({ statusCode: res.statusCode || 200, headers: res.headers, body: Buffer.concat(chunks) }); }); }); req.on('error', (error) => { console.error('🐘 PHP Built-In Server HTTP errored', error); resolve({ body: Buffer.from(`PHP Built-In Server HTTP error: ${error}`), headers: {}, statusCode: 500 }); }); if (body) { req.write(body); } req.end(); }); } function whenPortOpensCallback(port: number, attempts: number, cb: (error?: string) => void) { const client = net.connect(port, '127.0.0.1'); client.on('error', (error: string) => { if (!attempts) return cb(error); setTimeout(() => { whenPortOpensCallback(port, attempts - 1, cb); }, 10); }); client.on('connect', () => { client.destroy(); cb(); }); } function whenPortOpens(port: number, attempts: number): Promise { return new Promise((resolve, reject) => { whenPortOpensCallback(port, attempts, (error?: string) => { if (error) { reject(error); } else { resolve(); } }); }); } async function launcher(event: Event): Promise { const awsRequest = normalizeEvent(event); const input = await transformFromAwsRequest(awsRequest); const output = await query(input); return transformToAwsResponse(output); } exports.launcher = launcher; // (async function () { // const response = await launcher({ // Action: "test", // httpMethod: "GET", // body: "", // path: "/", // host: "https://vercel.com", // headers: { // 'HOST': 'vercel.com' // }, // encoding: null, // }); // console.log(response); // })(); ================================================ FILE: src/launchers/cgi.ts ================================================ import { spawn, SpawnOptions } from 'child_process'; import { parse as urlParse } from 'url'; import { getPhpDir, getUserDir, normalizeEvent, transformFromAwsRequest, transformToAwsResponse, isDev } from './helpers'; function createCGIReq({ entrypoint, path, host, method, headers }: CgiInput): CgiRequest { const { query } = urlParse(path); const env: Env = { ...process.env, SERVER_ROOT: getUserDir(), DOCUMENT_ROOT: getUserDir(), SERVER_NAME: host, SERVER_PORT: 443, HTTPS: "On", REDIRECT_STATUS: 200, SCRIPT_NAME: entrypoint, REQUEST_URI: path, SCRIPT_FILENAME: entrypoint, PATH_TRANSLATED: entrypoint, REQUEST_METHOD: method, QUERY_STRING: query || '', GATEWAY_INTERFACE: "CGI/1.1", SERVER_PROTOCOL: "HTTP/1.1", PATH: process.env.PATH, SERVER_SOFTWARE: "Vercel PHP", LD_LIBRARY_PATH: process.env.LD_LIBRARY_PATH }; if (headers["content-length"]) { env.CONTENT_LENGTH = headers["content-length"]; } if (headers["content-type"]) { env.CONTENT_TYPE = headers["content-type"]; } if (headers["x-real-ip"]) { env.REMOTE_ADDR = headers["x-real-ip"]; } // expose request headers Object.keys(headers).forEach(function (header) { var name = "HTTP_" + header.toUpperCase().replace(/-/g, "_"); env[name] = headers[header]; }); return { env } } function parseCGIResponse(response: Buffer) { const headersPos = response.indexOf("\r\n\r\n"); if (headersPos === -1) { return { headers: {}, body: response, statusCode: 200 } } let statusCode = 200; const rawHeaders = response.slice(0, headersPos).toString(); const rawBody = response.slice(headersPos + 4); const headers = parseCGIHeaders(rawHeaders); if (headers['status']) { statusCode = parseInt(headers['status']) || 200; } return { headers, body: rawBody, statusCode } } function parseCGIHeaders(headers: string): CgiHeaders { if (!headers) return {} const result: CgiHeaders = {} for (let header of headers.split("\n")) { const index = header.indexOf(':'); const key = header.slice(0, index).trim().toLowerCase(); const value = header.slice(index + 1).trim(); // Be careful about header duplication result[key] = value; } return result } function query({ entrypoint, path, host, headers, method, body }: PhpInput): Promise { console.log(`🐘 Spawning: PHP CGI ${entrypoint}`); // Transform lambda request to CGI variables const { env } = createCGIReq({ entrypoint, path, host, headers, method }) // php-cgi spawn options const options: SpawnOptions = { stdio: ['pipe', 'pipe', 'pipe'], env: env }; // now vs now-dev if (!isDev()) { options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; options.cwd = getPhpDir(); } else { options.cwd = getUserDir(); } return new Promise((resolve) => { const chunks: Uint8Array[] = []; const php = spawn( 'php-cgi', [entrypoint], options, ); // Validate pipes [stdin] if (!php.stdin) { console.error(`🐘 Fatal error. PHP CGI child process has no stdin.`); process.exit(253); } // Validate pipes [stdout] if (!php.stdout) { console.error(`🐘 Fatal error. PHP CGI child process has no stdout.`); process.exit(254); } // Validate pipes [stderr] if (!php.stderr) { console.error(`🐘 Fatal error. PHP CGI child process has no stderr.`); process.exit(255); } // Output php.stdout.on('data', data => { chunks.push(data); }); // Logging php.stderr.on('data', data => { console.error(`🐘 PHP CGI stderr`, data.toString()); }); // PHP script execution end php.on('close', (code, signal) => { if (code !== 0) { console.log(`🐘 PHP CGI process closed code ${code} and signal ${signal}`); } const { headers, body, statusCode } = parseCGIResponse(Buffer.concat(chunks)); resolve({ body, headers, statusCode }); }); php.on('error', err => { console.error('🐘 PHP CGI errored', err); resolve({ body: Buffer.from(`🐘 PHP CGI process errored ${err}`), headers: {}, statusCode: 500 }); }); // Writes the body into the PHP stdin php.stdin.write(body || ''); php.stdin.end(); }) } async function launcher(event: Event): Promise { const awsRequest = normalizeEvent(event); const input = await transformFromAwsRequest(awsRequest); const output = await query(input); return transformToAwsResponse(output); } exports.createCGIReq = createCGIReq; exports.launcher = launcher; // (async function () { // const response = await launcher({ // Action: "test", // httpMethod: "GET", // body: "", // path: "/", // host: "https://vercel.com", // headers: { // 'HOST': 'vercel.com' // }, // encoding: null, // }); // console.log(response); // })(); ================================================ FILE: src/launchers/cli.ts ================================================ import { spawn, SpawnOptions } from 'child_process'; import { getPhpDir, normalizeEvent, transformFromAwsRequest, transformToAwsResponse, isDev, getUserDir } from './helpers'; function query({ entrypoint, body }: PhpInput): Promise { console.log(`🐘 Spawning: PHP CLI ${entrypoint}`); // php spawn options const options: SpawnOptions = { stdio: ['pipe', 'pipe', 'pipe'], env: process.env }; // now vs now-dev if (!isDev()) { options.env!.PATH = `${getPhpDir()}:${process.env.PATH}`; options.cwd = getPhpDir(); } else { options.cwd = getUserDir(); } return new Promise((resolve) => { const chunks: Uint8Array[] = []; const php = spawn( 'php', ['-c', 'php.ini', entrypoint], options, ); // Validate pipes [stdin] if (!php.stdin) { console.error(`🐘 Fatal error. PHP CLI child process has no stdin.`); process.exit(253); } // Validate pipes [stdout] if (!php.stdout) { console.error(`🐘 Fatal error. PHP CLI child process has no stdout.`); process.exit(254); } // Validate pipes [stderr] if (!php.stderr) { console.error(`🐘 Fatal error. PHP CLI child process has no stderr.`); process.exit(255); } // Output php.stdout.on('data', data => { chunks.push(data); }); // Logging php.stderr.on('data', data => { console.error(`🐘 PHP CLI stderr`, data.toString()); }); // PHP script execution end php.on('close', (code, signal) => { if (code !== 0) { console.log(`🐘 PHP CLI process closed code ${code} and signal ${signal}`); } resolve({ statusCode: 200, headers: {}, body: Buffer.concat(chunks) }); }); php.on('error', err => { console.error('🐘 PHP CLI errored', err); resolve({ body: Buffer.from(`🐘 PHP CLI process errored ${err}`), headers: {}, statusCode: 500 }); }); // Writes the body into the PHP stdin php.stdin.write(body || ''); php.stdin.end(); }) } async function launcher(event: Event): Promise { const awsRequest = normalizeEvent(event); const input = await transformFromAwsRequest(awsRequest); const output = await query(input); return transformToAwsResponse(output); } exports.launcher = launcher; // (async function () { // const response = await launcher({ // Action: "test", // httpMethod: "GET", // body: "", // path: "/", // host: "https://vercel.com", // headers: { // 'HOST': 'vercel.com' // }, // encoding: null, // }); // console.log(response); // })(); ================================================ FILE: src/launchers/helpers.ts ================================================ import { join as pathJoin } from 'path'; export const getUserDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'user'); export const getPhpDir = (): string => pathJoin(process.env.LAMBDA_TASK_ROOT || '/', 'php'); export const isDev = (): boolean => process.env.NOW_PHP_DEV === '1'; export function normalizeEvent(event: Event): AwsRequest { if (event.Action === 'Invoke') { const invokeEvent = JSON.parse(event.body); const { method, path, host, headers = {}, encoding, }: InvokedEvent = invokeEvent; let { body } = invokeEvent; if (body) { if (encoding === 'base64') { body = Buffer.from(body, encoding); } else if (encoding === undefined) { body = Buffer.from(body); } else { throw new Error(`Unsupported encoding: ${encoding}`); } } return { method, path, host, headers, body, }; } const { httpMethod: method, path, host, headers = {}, body, } = event; return { method, path, host, headers, body, }; } export async function transformFromAwsRequest({ method, path, host, headers, body, }: AwsRequest): Promise { if (!process.env.NOW_ENTRYPOINT) { console.error('Missing ENV NOW_ENTRYPOINT'); } const entrypoint = pathJoin( getUserDir(), process.env.NOW_ENTRYPOINT || 'index.php', ); const uri = host + path; return { entrypoint, uri, path, host, method, headers, body }; } export function transformToAwsResponse({ statusCode, headers, body }: PhpOutput): AwsResponse { return { statusCode, headers, body: body.toString('base64'), encoding: 'base64' }; } ================================================ FILE: src/types.d.ts ================================================ type Headers = { [k: string]: string | string[] | undefined }; interface UserFiles { [filePath: string]: import('@vercel/build-utils').File; } interface RuntimeFiles { [filePath: string]: import('@vercel/build-utils').File; } interface IncludedFiles { [filePath: string]: import('@vercel/build-utils').File; } interface MetaOptions { meta: import('@vercel/build-utils').Meta; } interface AwsRequest { method: string, path: string, host: string, headers: Headers, body: string, } interface AwsResponse { statusCode: number, headers: Headers, body: string, encoding?: string } interface Event { Action: string, body: string, httpMethod: string, path: string, host: string, headers: Headers, encoding: string | undefined | null, } interface InvokedEvent { method: string, path: string, host: string, headers: Headers, encoding: string | undefined | null, } interface CgiInput { entrypoint: string, path: string, host: string, method: string, headers: Headers, } interface PhpInput { entrypoint: string, path: string, uri: string, host: string, method: string, headers: Headers, body: string, } interface PhpOutput { statusCode: number, headers: Headers, body: Buffer, } interface CgiHeaders { [k: string]: string, } interface CgiRequest { env: Env, } interface Env { [k: string]: any, } interface PhpIni { [k: string]: any, } ================================================ FILE: src/utils.ts ================================================ import path from 'path'; import { spawn, SpawnOptions } from 'child_process'; import { File, FileFsRef, FileBlob } from '@vercel/build-utils'; import * as libphp from "@libphp/almalinux-9-v85"; const PHP_PKG = path.dirname(require.resolve('@libphp/almalinux-9-v85/package.json')); const PHP_BIN_DIR = path.join(PHP_PKG, "native/php"); const PHP_MODULES_DIR = path.join(PHP_BIN_DIR, "modules"); const PHP_LIB_DIR = path.join(PHP_PKG, "native/lib"); const COMPOSER_BIN = path.join(PHP_BIN_DIR, "composer"); export async function getPhpFiles(): Promise { const files = await libphp.getFiles(); // Drop CGI + FPM from libphp, it's not needed for our case delete files['php/php-cgi']; delete files['php/php-fpm']; delete files['php/php-fpm.ini']; const runtimeFiles: RuntimeFiles = {}; // Map from @libphp to Vercel's File objects for (const [filename, filepath] of Object.entries(files)) { runtimeFiles[filename] = new FileFsRef({ fsPath: filepath }) } // Set some bins executable (runtimeFiles['php/php'] as FileFsRef).mode = 33261; // 0755; (runtimeFiles['php/composer'] as FileFsRef).mode = 33261; // 0755; return runtimeFiles; } export function getLauncherFiles(): RuntimeFiles { const files: RuntimeFiles = { 'helpers.js': new FileFsRef({ fsPath: path.join(__dirname, 'launchers/helpers.js'), }) } files['launcher.js'] = new FileFsRef({ fsPath: path.join(__dirname, 'launchers/builtin.js'), }); return files; } export async function modifyPhpIni(userFiles: UserFiles, runtimeFiles: RuntimeFiles): Promise { // Validate user files contains php.ini if (!userFiles['api/php.ini']) return; // Validate runtime contains php.ini if (!runtimeFiles['php/php.ini']) return; const phpiniBlob = await FileBlob.fromStream({ stream: runtimeFiles['php/php.ini'].toStream(), }); const userPhpiniBlob = await FileBlob.fromStream({ stream: userFiles['api/php.ini'].toStream(), }); return new FileBlob({ data: phpiniBlob.data.toString() .concat("; [User]\n") .concat(userPhpiniBlob.data.toString()) }); } export async function runComposerInstall(workPath: string): Promise { console.log('🐘 Installing Composer dependencies [START]'); // @todo PHP_COMPOSER_INSTALL env await runPhp( [ COMPOSER_BIN, 'install', '--profile', '--no-dev', '--no-interaction', '--no-scripts', '--ignore-platform-reqs', '--no-progress' ], { stdio: 'inherit', cwd: workPath } ); console.log('🐘 Installing Composer dependencies [DONE]'); } export async function runComposerScripts(composerFile: File, workPath: string): Promise { let composer; try { composer = JSON.parse(await readRuntimeFile(composerFile)); } catch (e) { console.error('🐘 Composer file is not valid JSON'); console.error(e); return; } if (composer?.scripts?.vercel) { console.log('🐘 Running composer scripts [START]'); await runPhp( [COMPOSER_BIN, 'run', 'vercel'], { stdio: 'inherit', cwd: workPath } ); console.log('🐘 Running composer scripts [DONE]'); } } export async function ensureLocalPhp(): Promise { try { await spawnAsync('which', ['php', 'php-cgi'], { stdio: 'pipe' }); return true; } catch (e) { return false; } } export async function readRuntimeFile(file: File): Promise { const blob = await FileBlob.fromStream({ stream: file.toStream(), }); return blob.data.toString(); } // ***************************************************************************** // PRIVATE API ***************************************************************** // ***************************************************************************** async function runPhp(args: ReadonlyArray, opts: SpawnOptions = {}) { try { await spawnAsync('php', args, { ...opts, env: { ...process.env, ...(opts.env || {}), COMPOSER_HOME: '/tmp', PATH: `${PHP_BIN_DIR}:${process.env.PATH}`, PHP_INI_EXTENSION_DIR: PHP_MODULES_DIR, PHP_INI_SCAN_DIR: `:${path.resolve(__dirname, '../conf')}`, LD_LIBRARY_PATH: `${PHP_LIB_DIR}:/usr/lib64:/lib64:${process.env.LD_LIBRARY_PATH}` } } ); } catch (e) { console.error(e); process.exit(1); } } function spawnAsync(command: string, args: ReadonlyArray, opts: SpawnOptions = {}): Promise { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: "ignore", ...opts }); child.on('error', reject); child.on('exit', (code, signal) => { if (code === 0) { resolve(); } else { reject(new Error(`Exited with ${code || signal}`)); } }); }) } ================================================ FILE: test/examples/00-php/.gitignore ================================================ # Vercel .vercel ================================================ FILE: test/examples/00-php/api/api/index.php ================================================ 1, 'name' => 'f3l1x'], ['id' => 2, 'name' => 'chemix'], ['id' => 3, 'name' => 'dg'], ['id' => 4, 'name' => 'milo'], ['id' => 5, 'name' => 'matej21'], ['id' => 6, 'name' => 'merxes'], ]; header('Content-Type: application/json'); echo json_encode($data); ================================================ FILE: test/examples/00-php/api/ext/ds.php ================================================ 1, "b" => 2, "c" => 3]); $map->apply(function($key, $value) { return $value * 2; }); print_r($map); ?> ================================================ FILE: test/examples/00-php/api/ext/gd.php ================================================ ================================================ FILE: test/examples/00-php/api/ext/index.php ================================================ ================================================ FILE: test/examples/00-php/api/ext/phalcon.php ================================================ get( "/", function () { echo "

Welcome!

"; } ); $app->get( "/say/hello/{name}", function ($name) use ($app) { echo "

Hello! $name

"; echo "Your IP Address is ", $app->request->getClientAddress(); } ); $app->post( "/store/something", function () use ($app) { $name = $app->request->getPost("name"); echo "

Hello! $name

"; } ); $app->notFound( function () use ($app) { $app->response->setStatusCode(404, "Not Found"); $app->response->sendHeaders(); echo "This is crazy, but this page was not found!"; } ); $app->handle(); ================================================ FILE: test/examples/00-php/api/hello.php ================================================ php.ini $group) { echo ""; echo sprintf('', $name); echo sprintf('', $group['global_value']); echo sprintf('', $group['local_value']); echo sprintf('', $group['access']); echo ""; } ?>
option global_value local_value access
%s%s%s%s
================================================ FILE: test/examples/00-php/api/libs.php ================================================ "; } else { $files = scandir($path); echo "Scan folder: $path
"; array_map(function($file) { echo "- $file
"; }, $files); } echo "
"; } ================================================ FILE: test/examples/00-php/api/test.php ================================================ ================================================ FILE: test/examples/02-extensions/vercel.json ================================================ { "version": 2, "builds": [{ "src": "index.php", "use": "vercel-php@0.7.3" }] } ================================================ FILE: test/examples/03-env-vars/env/index.php ================================================ { const mockLog = console.log = jest.fn(); jest.spyOn(process, 'exit').mockImplementation((code) => { expect(code).toBe(255); expect(mockLog).toHaveBeenCalledTimes(1); }); await builder.build({ files: [], entrypoint: 'test.php', workPath: __dirname, config: {}, meta: { isDev: true }, }); }); ================================================ FILE: test/spec/index.js ================================================ const builder = require('./../../dist/index'); test('creates simple lambda', async () => { await builder.build({ files: [], entrypoint: 'test.php', workPath: __dirname, config: {}, meta: {}, }); }); ================================================ FILE: test/spec/launchers/cgi.js ================================================ const cgi = require('./../../../dist/launchers/cgi'); test('create CGI request', () => { const request = { entrypoint: "index.php", path: "/index.php", host: "https://vercel.com", method: "GET", headers: {} }; process.env.CUSTOM_VALUE = "custom-value"; const { env } = cgi.createCGIReq(request); expect(env).toHaveProperty("SERVER_ROOT", "/user"); expect(env).toHaveProperty("DOCUMENT_ROOT", "/user"); expect(env).toHaveProperty("SERVER_NAME", request.host); expect(env).toHaveProperty("SERVER_PORT", 443); expect(env).toHaveProperty("HTTPS", 'On'); expect(env).toHaveProperty("REDIRECT_STATUS", 200); expect(env).toHaveProperty("SCRIPT_NAME", request.entrypoint); expect(env).toHaveProperty("REQUEST_URI", request.path); expect(env).toHaveProperty("SCRIPT_FILENAME", request.entrypoint); expect(env).toHaveProperty("PATH_TRANSLATED", request.entrypoint); expect(env).toHaveProperty("REQUEST_METHOD", request.method); expect(env).toHaveProperty("QUERY_STRING", ''); expect(env).toHaveProperty("GATEWAY_INTERFACE", 'CGI/1.1'); expect(env).toHaveProperty("SERVER_PROTOCOL", 'HTTP/1.1'); expect(env).toHaveProperty("SERVER_SOFTWARE", 'Vercel PHP'); expect(env).toHaveProperty("PATH", process.env.PATH); expect(env).toHaveProperty("LD_LIBRARY_PATH", process.env.LD_LIBRARY_PATH); expect(env).toHaveProperty("CUSTOM_VALUE", process.env.CUSTOM_VALUE); }); ================================================ FILE: test/spec/path.js ================================================ const path = require('path'); test('relative path', () => { const rootdir = '/var/task/user'; const request = '/var/task/user/api/users.php'; const file = path.relative(rootdir, request); expect(file).toBe('api/users.php'); }); ================================================ FILE: test/spec/url.js ================================================ const url = require('url'); test('url.parse search & query are string', () => { const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo'); expect(search).toBe('?foo=bar&foo2=baz'); expect(query).toBe('foo=bar&foo2=baz'); }); test('url.parse search string, query object', () => { const { search, query } = url.parse('https://vercel.com/?foo=bar&foo2=baz#foo', true); expect(search).toBe('?foo=bar&foo2=baz'); expect(query).toMatchObject({ foo: 'bar', 'foo2': 'baz' }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "strict": true, "esModuleInterop": true, "resolveJsonModule": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "lib": [ "ES2019" ], "target": "ES2019", "module": "CommonJS", "outDir": "dist", "sourceMap": false, "declaration": true, "noImplicitAny": true, "noUnusedLocals": true, "noUnusedParameters": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, "typeRoots": [ "./node_modules/@types" ], "types": [] }, "include": [ "src/**/*.ts" ], "exclude": [ "errors", "dist", "node_modules", "test" ] }