Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8045011
simply use the built-in php-cli
withinboredom Jul 19, 2025
44408b6
refactor code around calling do_php_cli
henderkes Aug 3, 2025
2b75b00
move old cli sapi emulation out to emulate_php_cli.c
henderkes Aug 3, 2025
a8762a0
EOF newlines
henderkes Aug 3, 2025
dce590c
shuffle things to work
henderkes Aug 3, 2025
e335244
fix 80500
henderkes Aug 3, 2025
d1bb029
Merge pull request #1782 from static-php/cli_sapi
henderkes Oct 19, 2025
a072fa1
Merge branch 'main' into refactor/php-cli
henderkes Mar 6, 2026
ebda59d
Merge branch 'main' into refactor/php-cli
henderkes Mar 6, 2026
4726593
clang format
henderkes Mar 6, 2026
9ca0358
update for new compilation in php-src
henderkes Mar 6, 2026
0f28cc0
fix cli test
henderkes Mar 6, 2026
30ff796
remove duplicate test
henderkes Mar 6, 2026
c40a257
fix expected args for php-cli, though this won't help the strangely f…
henderkes Mar 6, 2026
82a62b0
executephpcode
henderkes Mar 6, 2026
1cbc658
move script detection to C side, not Go side (because we only want to…
henderkes Mar 6, 2026
e2b6113
fix on windows?
henderkes Mar 6, 2026
72633ef
php 8.6
henderkes Mar 7, 2026
dc9dd5f
undefined references
henderkes Mar 7, 2026
5bab8f5
fix windows again
henderkes Mar 7, 2026
e19983c
Merge remote-tracking branch 'origin/main' into refactor/php-cli
henderkes Jul 28, 2026
89628f0
fix borked merge
henderkes Jul 28, 2026
67c97eb
Update emulate_php_cli.c
henderkes Jul 28, 2026
cd7a908
move var
henderkes Jul 28, 2026
c4ddc34
clang format
henderkes Jul 28, 2026
a137c20
bool undefined
henderkes Jul 28, 2026
6ac40fd
fix windows
henderkes Jul 28, 2026
93e774a
just use intsafe.h
henderkes Jul 28, 2026
3a6c5e6
include bool
henderkes Jul 28, 2026
eaeba14
add test for internal cli emulation not trying to run -i/-v/etc. in p…
henderkes Jul 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions caddy/php-cli.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package caddy

import (
"errors"
"os"
"path/filepath"
"strings"

caddycmd "github.com/caddyserver/caddy/v2/cmd"
"github.com/dunglas/frankenphp"
Expand All @@ -26,23 +26,16 @@ Executes a PHP script similarly to the CLI SAPI.`,
}

func cmdPHPCLI(fs caddycmd.Flags) (int, error) {
args := os.Args[2:]
if len(args) < 1 {
return 1, errors.New("the path to the PHP script is required")
}
// php's cli sapi expects the 0th arg to be the program itself, only filter out 'php-cli' arg
args := append([]string{os.Args[0]}, os.Args[2:]...)

if frankenphp.EmbeddedAppPath != "" {
if _, err := os.Stat(args[0]); err != nil {
args[0] = filepath.Join(frankenphp.EmbeddedAppPath, args[0])
if frankenphp.EmbeddedAppPath != "" && len(args) > 1 && !strings.HasPrefix(args[1], "-") && strings.HasSuffix(args[1], ".php") {
if _, err := os.Stat(args[1]); err != nil {
args[1] = filepath.Join(frankenphp.EmbeddedAppPath, args[1])
}
}

var status int
if len(args) >= 2 && args[0] == "-r" {
status = frankenphp.ExecutePHPCode(args[1])
} else {
status = frankenphp.ExecuteScriptCLI(args[0], args)
}
status := frankenphp.ExecuteScriptCLI(args[0], args)

os.Exit(status)

Expand Down
2 changes: 1 addition & 1 deletion cgo.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ package frankenphp
// #cgo unix LDFLAGS: -lphp -lm -lutil
// #cgo linux LDFLAGS: -ldl -lresolv
// #cgo darwin LDFLAGS: -Wl,-rpath,/usr/local/lib -liconv -ldl
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256
// #cgo windows CFLAGS: -D_WINDOWS -DWINDOWS=1 -DZEND_WIN32=1 -DPHP_WIN32=1 -DWIN32 -D_MBCS -D_USE_MATH_DEFINES -DNDebug -DNDEBUG -DZEND_DEBUG=0 -DZTS=1 -DFD_SETSIZE=256 -DENABLE_INTSAFE_SIGNED_FUNCTIONS
// #cgo windows LDFLAGS: -lpthreadVC3
import "C"
9 changes: 0 additions & 9 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,3 @@ func ExecuteScriptCLI(script string, args []string) int {

return int(C.frankenphp_execute_script_cli(cScript, argc, (**C.char)(unsafe.Pointer(&argv[0])), false))
}

func ExecutePHPCode(phpCode string) int {
// Ensure extensions are registered before CLI execution
registerExtensions()

cCode := C.CString(phpCode)
defer C.free(unsafe.Pointer(cCode))
return int(C.frankenphp_execute_script_cli(cCode, 0, nil, true))
}
30 changes: 29 additions & 1 deletion cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,34 @@ func TestExecuteCLICode(t *testing.T) {
assert.Equal(t, stdoutStderrStr, `Hello World`)
}

// `-i` (and any other invocation without a script) is only supported since PHP
// 8.6, where the real CLI SAPI is reused. older versions must fail cleanly.
func TestExecuteCLIPHPInfo(t *testing.T) {
if _, err := os.Stat("internal/testcli/testcli"); err != nil {
t.Skip("internal/testcli/testcli has not been compiled, run `cd internal/testcli/ && go build`")
}

cmd := exec.Command("internal/testcli/testcli", "-i")
stdoutStderr, err := cmd.CombinedOutput()
stdoutStderrStr := string(stdoutStderr)

if frankenphp.Version().VersionID < 80600 {
assert.Error(t, err)

var exitError *exec.ExitError
if errors.As(err, &exitError) {
assert.Equal(t, 1, exitError.ExitCode())
}

assert.Contains(t, stdoutStderrStr, "this functionality is not available in frankenphp php-cli")

return
}

assert.NoError(t, err, "output: %s", stdoutStderrStr)
assert.Contains(t, stdoutStderrStr, "PHP Version => "+frankenphp.Version().Version)
}

// Regression test for https://github.com/php/frankenphp/issues/1902. A
// long-running CLI script that installs pcntl_signal handlers must
// receive its own signals reliably
Expand Down Expand Up @@ -73,5 +101,5 @@ func ExampleExecuteScriptCLI() {
os.Exit(1)
}

os.Exit(frankenphp.ExecuteScriptCLI(os.Args[1], os.Args))
os.Exit(frankenphp.ExecuteScriptCLI(os.Args[0], os.Args))
}
191 changes: 191 additions & 0 deletions emulate_php_cli.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include "emulate_php_cli.h"
#include "frankenphp.h"
#include <SAPI.h>
#include <Zend/zend_alloc.h>
#include <Zend/zend_exceptions.h>
#include <Zend/zend_interfaces.h>
#include <Zend/zend_types.h>
#include <errno.h>
#include <ext/spl/spl_exceptions.h>
#include <ext/standard/head.h>
#include <inttypes.h>
#include <php.h>
#ifdef PHP_WIN32
#include <config.w32.h>
#else
#include <php_config.h>
#endif
#include <php_ini.h>
#include <php_main.h>
#include <php_output.h>
#include <php_variables.h>
#include <php_version.h>
#include <pthread.h>
#include <sapi/embed/php_embed.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef ZEND_WIN32
#include <unistd.h>
#endif
#if defined(__linux__)
#include <sys/prctl.h>
#elif defined(__FreeBSD__) || defined(__OpenBSD__)
#include <pthread_np.h>
#endif

cli_exec_args_t *cli_args;

static void register_server_variable_filtered(const char *key, char **val,
size_t *val_len,
zval *track_vars_array) {
if (sapi_module.input_filter(PARSE_SERVER, key, val, *val_len, val_len)) {
php_register_variable_safe(key, *val, *val_len, track_vars_array);
}
}

/*
* CLI code is adapted from
* https://github.com/php/php-src/blob/master/sapi/cli/php_cli.c Copyright (c)
* The PHP Group Licensed under The PHP License Original uthors: Edin Kadribasic
* <edink@php.net>, Marcus Boerger <helly@php.net> and Johannes Schlueter
* <johannes@php.net> Parts based on CGI SAPI Module by Rasmus Lerdorf, Stig
* Bakken and Zeev Suraski
*/
static void cli_register_file_handles(bool no_close) /* {{{ */
{
php_stream *s_in, *s_out, *s_err;
php_stream_context *sc_in = NULL, *sc_out = NULL, *sc_err = NULL;
zend_constant ic, oc, ec;

s_in = php_stream_open_wrapper_ex("php://stdin", "rb", 0, NULL, sc_in);
s_out = php_stream_open_wrapper_ex("php://stdout", "wb", 0, NULL, sc_out);
s_err = php_stream_open_wrapper_ex("php://stderr", "wb", 0, NULL, sc_err);

if (s_in == NULL || s_out == NULL || s_err == NULL) {
if (s_in)
php_stream_close(s_in);
if (s_out)
php_stream_close(s_out);
if (s_err)
php_stream_close(s_err);
return;
}

if (no_close) {
s_in->flags |= PHP_STREAM_FLAG_NO_CLOSE;
s_out->flags |= PHP_STREAM_FLAG_NO_CLOSE;
s_err->flags |= PHP_STREAM_FLAG_NO_CLOSE;
}

/*s_in_process = s_in;*/

php_stream_to_zval(s_in, &ic.value);
php_stream_to_zval(s_out, &oc.value);
php_stream_to_zval(s_err, &ec.value);

ZEND_CONSTANT_SET_FLAGS(&ic, CONST_CS, 0);
ic.name = zend_string_init_interned("STDIN", sizeof("STDIN") - 1, 0);
zend_register_constant(&ic);

ZEND_CONSTANT_SET_FLAGS(&oc, CONST_CS, 0);
oc.name = zend_string_init_interned("STDOUT", sizeof("STDOUT") - 1, 0);
zend_register_constant(&oc);

ZEND_CONSTANT_SET_FLAGS(&ec, CONST_CS, 0);
ec.name = zend_string_init_interned("STDERR", sizeof("STDERR") - 1, 0);
zend_register_constant(&ec);
}
/* }}} */

static void sapi_cli_register_variables(zval *track_vars_array) /* {{{ */
{
size_t len = strlen(cli_args->script);
char *docroot = "";

/*
* In CGI mode, we consider the environment to be a part of the server
* variables
*/
php_import_environment_variables(track_vars_array);

/* Build the special-case PHP_SELF variable for the CLI version */
register_server_variable_filtered("PHP_SELF", &cli_args->script, &len,
track_vars_array);
register_server_variable_filtered("SCRIPT_NAME", &cli_args->script, &len,
track_vars_array);

/* filenames are empty for stdin */
register_server_variable_filtered("SCRIPT_FILENAME", &cli_args->script, &len,
track_vars_array);
register_server_variable_filtered("PATH_TRANSLATED", &cli_args->script, &len,
track_vars_array);

/* just make it available */
len = 0U;
register_server_variable_filtered("DOCUMENT_ROOT", &docroot, &len,
track_vars_array);
}
/* }}} */

void *emulate_script_cli(void *arg) {
void *exit_status;
cli_exec_args_t *args = arg;
cli_args = args;

/* Parse argv to detect -r (eval mode) and find the script path */
bool eval = false;
char *script = NULL;
for (int i = 1; i < args->argc; i++) {
if (strcmp(args->argv[i], "-r") == 0 && i + 1 < args->argc) {
eval = true;
script = args->argv[i + 1];
break;
} else if (args->argv[i][0] != '-') {
script = args->argv[i];
break;
}
}

if (script == NULL) {
fprintf(stderr, "this functionality is not available in frankenphp php-cli "
"with a php version lower than 8.6\n");
return (void *)(intptr_t)1;
}

/* Update cli_args->script so sapi_cli_register_variables uses the right path
*/
cli_args->script = script;

/*
* The SAPI name "cli" is hardcoded into too many programs... let's usurp it.
*/
php_embed_module.name = "cli";
php_embed_module.pretty_name = "PHP CLI embedded in FrankenPHP";
php_embed_module.register_server_variables = sapi_cli_register_variables;

php_embed_init(cli_args->argc, cli_args->argv);

cli_register_file_handles(false);
zend_first_try {
if (eval) {
/* evaluate script as literal PHP code (php-cli -r "...") */
zend_eval_string_ex(script, NULL, "Command line code", 1);
} else {
zend_file_handle file_handle;
zend_stream_init_filename(&file_handle, script);

CG(skip_shebang) = 1;
php_execute_script(&file_handle);
}
}
zend_end_try();

exit_status = (void *)(intptr_t)EG(exit_status);

php_embed_shutdown();

return exit_status;
}
15 changes: 15 additions & 0 deletions emulate_php_cli.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#ifndef _EMULATE_PHP_CLI_H
#define _EMULATE_PHP_CLI_H

#include <stdbool.h>

typedef struct {
char *script;
int argc;
char **argv;
bool eval;
} cli_exec_args_t;
extern cli_exec_args_t *cli_args;
void *emulate_script_cli(void *arg);

#endif
Loading
Loading