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

## 🎯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
| option |
global_value |
local_value |
access |
$group) {
echo "";
echo sprintf('| %s | ', $name);
echo sprintf('%s | ', $group['global_value']);
echo sprintf('%s | ', $group['local_value']);
echo sprintf('%s | ', $group['access']);
echo "
";
}
?>
================================================
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"
]
}