PHPRunning PHP Scripts (CLI & Browser)

Running PHP Scripts (CLI & Browser)

PHP is not one program that behaves one way — it is an engine that gets embedded into different "SAPIs" (Server APIs), each of which wires up the language differently depending on who is asking for output. The two you will meet constantly are the CLI SAPI, used when you type php somefile.php in a terminal, and the web SAPI, used when Apache's mod_php or a PHP-FPM pool hands PHP an incoming HTTP request. Same language, same functions, same syntax — but very different surrounding environment. Confusing the two is a common source of "why is $_GET empty" and "why does this cron job print nothing" bugs, so it is worth understanding exactly what each context gives you and takes away.

Two SAPIs, one language

You can check which SAPI a given php binary is currently running under with php_sapi_name(), or just look at php -v, which prints it directly.

Terminal

Bash
php -v
PHP 8.3.4 (cli) (built: Feb 21 2026 10:12:03) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.3.4, Copyright (c) Zend Technologies

The (cli) in that output is the tell — this build is running under the CLI SAPI. A web request handled by Apache's mod_php module or a PHP-FPM worker process reports (apache2handler) or (fpm-fcgi) instead when you inspect it with phpinfo() or php_sapi_name() from inside a script.

Superglobals: what exists where

This is the part that trips people up in practice. PHP's superglobal arrays are always defined, but several of them are only ever populated when an actual HTTP request triggered the script.

Superglobal

CLI SAPI

Web SAPI

$_GET / $_POST

Always empty — there is no HTTP request to parse a query string or body from.

Populated from the request URL / form body.

$_SESSION / $_COOKIE

Empty (no cookies were sent by a browser); sessions technically work but are rarely useful without a client.

Populated from the Cookie header; sessions persist across requests.

$_SERVER

Populated, but with process/environment info (argv, PHP_SELF, shell env vars) rather than HTTP details.

Populated with HTTP request details: REQUEST_METHOD, HTTP_USER_AGENT, REMOTE_ADDR, etc.

$argv / $argc

Available — the array of command-line arguments and their count.

Undefined — there is no command line invoking the script.

whereami.php — inspecting the environment at runtime

PHP
<?php

echo "SAPI: " . php_sapi_name() . "\n";
echo "argv is set: " . (isset($argv) ? 'yes' : 'no') . "\n";
echo "\$_GET is empty: " . (empty($_GET) ? 'yes' : 'no') . "\n";

Run from the terminal

Bash
php whereami.php
SAPI: cli
argv is set: yes
$_GET is empty: yes

Put the exact same file behind a web server and load it in a browser, and the story flips: php_sapi_name() reports fpm-fcgi or apache2handler, $argv is undefined (accessing it triggers an "undefined variable" notice), and $_GET is populated the moment the URL includes a query string.

Command-line arguments: $argv and $argc

In CLI mode, everything typed after the script name on the command line lands in the $argv array ($argv[0] is always the script's own filename), and $argc holds the total element count. This is how CLI PHP scripts accept input, since there is no form or URL to read from.

greet.php

PHP
<?php

if ($argc < 2) {
    fwrite(STDERR, "Usage: php greet.php <name>\n");
    exit(1);
}

$name = $argv[1];
echo "Hello, {$name}! You passed {$argc} argument(s) total.\n";

Terminal

Bash
php greet.php Priya
Hello, Priya! You passed 2 argument(s) total.

exit(1) there is deliberate: CLI scripts communicate success or failure to whatever invoked them (a shell script, a cron scheduler, a CI pipeline) through the process exit code, not through HTTP status codes. 0 means success, any nonzero value signals an error — this convention barely matters for a web page but is essential for anything scripted or automated.

Common CLI flags

Flag

Purpose

Example

-f

Explicitly run a file. Almost always omitted, since php somefile.php implies it.

php -f script.php

-r

Run a snippet of code passed directly on the command line — no file needed, and no opening <?php tag either.

php -r "echo 2 + 2;"

-l

Lint: check a file for syntax errors without executing a single line of it.

php -l script.php

-v

Print the PHP version and build information, then exit.

php -v

-m

List every extension/module compiled into or loaded by this PHP build.

php -m

-S

Start PHP's built-in development web server on a given host:port — a lightweight web SAPI for local testing, no Apache/Nginx required.

php -S localhost:8000

A few of these in action

Bash
# Quick one-off calculation, no file needed
php -r "echo 2 + 2;"

# Syntax-check a file before deploying it, without running it
php -l deploy/migrate.php

# See which extensions are available (e.g. checking for pdo_mysql)
php -m | grep pdo
4
No syntax errors detected in deploy/migrate.php
pdo_mysql

-l is worth building into a habit: it parses the file and reports any fatal syntax error without ever executing the code, which makes it a cheap, safe pre-flight check to run in a pre-commit hook or a CI step before a script that deletes rows or modifies files ever gets a chance to run.

Choosing the right context for the job
  • CLI is the right tool for: cron jobs and scheduled maintenance, one-off data-fixing scripts, Composer and other package-manager tooling, automated test suites (PHPUnit, Pest), build/deploy scripts, and database backup or migration utilities.

  • Web SAPI is the right tool for: anything a browser or an HTTP client (a frontend app, a mobile app, another service) needs to reach directly — rendered pages, JSON APIs, webhook receivers, file upload handlers.

  • Rule of thumb: if the task's input is $argv and its output is meant for a human at a terminal or a log file, write it as a CLI script. If its input is an HTTP request and its output is meant for a browser or API client, write it for the web SAPI.

backup-database.php — this only makes sense as a CLI script

PHP
<?php

// A destructive-adjacent script: dumps the production database to disk.
// Never expose this over HTTP — see the Warning below.

$dbName = $argv[1] ?? null;
if ($dbName === null) {
    fwrite(STDERR, "Usage: php backup-database.php <database_name>\n");
    exit(1);
}

$timestamp = date('Y-m-d_His');
$outputFile = "/var/backups/{$dbName}_{$timestamp}.sql";

$command = sprintf(
    'mysqldump --single-transaction %s > %s',
    escapeshellarg($dbName),
    escapeshellarg($outputFile)
);

exec($command, $output, $exitCode);

if ($exitCode !== 0) {
    fwrite(STDERR, "Backup failed with exit code {$exitCode}\n");
    exit($exitCode);
}

echo "Backup written to {$outputFile}\n";
Never expose an operational script like this over the web
If `backup-database.php` (or anything that runs `exec()`, deletes rows, or drops tables) were ever placed inside a web server's document root, anyone who discovers the URL could trigger it with a plain browser request — no authentication check would stop them unless you built one, and there is no shell, no cron scheduler, and no trusted operator standing between the request and the code. Scripts like this belong outside the document root entirely, invoked only by `cron`, a deploy pipeline, or a developer at a terminal — never reachable by URL.
PHP's built-in development server (-S)

The -S flag starts a minimal, single-threaded web SAPI without installing Apache or Nginx — genuinely useful for local development and quick demos, though not something to run in production. It is covered in more depth on its own page; the short version is that it turns your CLI PHP install into a temporary local web server for whatever directory you point it at.

Terminal

Bash
php -S localhost:8000
# PHP 8.3.4 Development Server started at ...
# Listening on http://localhost:8000
# Document root is /home/you/project
# Press Ctrl-C to quit.
How to tell which SAPI is currently running your code
Call `php_sapi_name()` from inside any script. It returns a plain string — `cli` for the command line, `cli-server` for the `-S` built-in server, `fpm-fcgi` for PHP-FPM, or `apache2handler` for `mod_php`. Branching your application logic on this value is rarely a good idea, but it is a genuinely useful debugging check when you are not sure why a script is behaving differently in two environments.
Tip
Add `php -l` on every changed `.php` file as a pre-commit or CI check. It costs milliseconds, catches an entire class of "forgot a semicolon" deploy failures before they ever reach a server, and never actually executes the code it is checking — so it is safe to run against scripts that would otherwise send emails, charge payments, or touch a production database.