Running PHP: CLI, the Built-in Server and php.ini
Running PHP while you learn and work: scripts on the command line, the interactive shell, the built-in development server, and the php.ini settings that change how PHP behaves.
Running scripts on the command line
The PHP CLI (command-line interface)
is the php program you run in a terminal. It executes a
PHP file from top to bottom and exits, just like
python3 or node. It is the same language
as on a web server, with a few differences that suit scripts: there
is no time limit, output goes to the terminal, and nothing is sent
as HTTP headers.
Like every command, php average.php looks for the file
in the terminal's
working directory, so first go to the folder that contains it with cd.
From the wrong folder PHP answers
Could not open input file: average.php.
cd ~/projects/php # the folder that contains average.php
php average.php # run a script
php --version # which PHP version is this?
php -r 'echo 7 * 6, PHP_EOL;' # run one line of code without a file
Days measured: 5
Average per day: 11.4 kWh
PHP 8.4.26 (cli) (built: …) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.4.26, Copyright (c) Zend Technologies
with Zend OPcache v8.4.26, Copyright (c), by Zend Technologies
42
If the command is not found, PHP is not installed, or not on the
PATH. On Debian and Ubuntu the package is
php-cli (or a versioned one such as
php8.4-cli); see
the apt workflow. On Windows and macOS, the PHP downloads page and package managers
such as winget and Homebrew provide it.
Arguments and exit codes
Words typed after the script name arrive in the array
$argv; $argv[0] is the script name itself
and $argc is the number of entries. A script reports
success or failure to the shell through its
exit status:
exit(0) (or reaching the end) means success, any other
number means failure. Error messages belong on standard error,
written with fwrite(STDERR, …), so they do not mix with
the real output.
<?php
// PHP fills $argv itself; here we set it to what this command would give:
// php usage.php 2026-10 north
$argv = ['usage.php', '2026-10', 'north'];
$argc = count($argv);
if ($argc !== 3) {
fwrite(STDERR, "Usage: php usage.php MONTH REGION\n");
exit(2);
}
[, $month, $region] = $argv;
echo "Usage report for $region, $month\n";
Usage report for north, 2026-10
Scripts can also read
standard input, so they fit into pipelines. STDIN is an open file
handle; fgets() reads one line and returns
false at the end:
<?php
// Reads one meter reading per line until the input ends
$total = 0.0;
$count = 0;
while (($line = fgets(STDIN)) !== false) {
$total += (float) trim($line);
$count++;
}
printf("%d readings, total %.1f kWh\n", $count, $total);
3 readings, total 33.5 kWh
cut -d';' -f3 readings.csv | php total.php
On Linux a script can start with a
shebang line,
#!/usr/bin/env php, above <?php; after
chmod +x you can run it as ./total.php.
Useful command-line options
| Command | What it does |
|---|---|
php -v / php --version |
Show the version |
php -l file.php |
Check the syntax (lint) without running the file |
php -r 'code' |
Run code given on the command line, without the tags |
php -a |
Start the interactive shell |
php -S host:port |
Start the built-in development web server |
php --ini |
Show which php.ini and extra .ini files are loaded |
php -i |
Print all configuration (the command-line version of
phpinfo())
|
php -m |
List the loaded extensions |
php -d name=value |
Override one php.ini setting for this run |
php -n |
Run without any php.ini (built-in defaults only) |
Checking syntax with php -l
php -l ("lint") parses a file and reports syntax errors
without running any of it, so it is safe on code that would send
emails or change a database. Run it before you upload a file that
was edited by hand: a single missing semicolon makes every page that
includes the file fail.
php -l public/index.php
php -l bad.php; echo "exit status: $?"
No syntax errors detected in public/index.php PHP Parse error: syntax error, unexpected token "echo", expecting "," or ";" in bad.php on line 3 Errors parsing bad.php exit status: 255
The line number points to where PHP noticed the problem;
the mistake (here a missing ;) is often at the end of
the line before. Since PHP 8.3, php -l accepts several
files at once; on older versions, lint one file per command. The
site's code examples are checked the same way.
The interactive shell
The
PHP interactive shell,
started with php -a, reads one statement at a time,
runs it and waits for the next. It is handy for trying a function or
checking how PHP treats a value. Two differences from Python's
REPL and the
Node.js console: every statement needs its semicolon, and a bare
expression shows nothing — you must echo or
var_dump() it.
php -a
Interactive shell
php > $price = 0.32;
php > echo $price * 350;
112
php > var_dump("5" + "5");
int(10)
php > exit
The shell needs the readline extension. Without it,
current PHP versions stop with "Interactive shell (-a) requires the
readline extension"; on Debian and Ubuntu install the
php8.x-readline package for your version. For longer
experiments many developers prefer a small file and
php test.php, or a richer shell such as PsySH (a
Composer package).
The built-in web server
The
built-in web server
is a small development server inside the PHP CLI, started with
php -S. It serves the files in a folder and runs
.php files through PHP, so you can try a site in your
browser without installing Apache or Nginx:
php -S localhost:8000 -t public
[Fri Oct 2 09:14:03 2026] PHP 8.4.26 Development Server (http://localhost:8000) started [Fri Oct 2 09:14:09 2026] [::1]:40968 Accepted [Fri Oct 2 09:14:09 2026] [::1]:40968 [200]: GET / [Fri Oct 2 09:14:09 2026] [::1]:40968 Closing [Fri Oct 2 09:14:09 2026] [::1]:40982 [404]: GET /missing.css - No such file or directory
-t public sets the document root (the folder that URLs
map to); without it, the current folder is used. A request for a
folder runs its index.php or serves
index.html. Every request is logged in the terminal,
which makes it easy to see a 404 for a missing stylesheet. Stop the
server with Ctrl+C. Inside a script on this
server, PHP_SAPI is "cli-server".
Router scripts
Frameworks send every URL to one front controller,
index.php, which decides what to show. The built-in
server does this with a router script given after the
address. The router runs for every request; if it returns
false, the server delivers the requested file as it is:
<?php
// router.php — started with: php -S localhost:8000 router.php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if (is_file(__DIR__ . $path)) {
return false; // an existing file (CSS, image): serve it unchanged
}
require __DIR__ . '/index.php'; // everything else goes to the front controller
Frameworks ship their own command for this, such as
php artisan serve in Laravel, which starts PHP's
built-in server for you.
Configuration: php.ini
php.ini is PHP's main configuration
file. It is read when PHP starts and sets limits (memory, upload
size, execution time), error handling, session behavior and which
extensions to load. The format is simple:
name = value lines, with ; starting a
comment.
; Maximum amount of memory a script may consume memory_limit = 128M display_errors = Off log_errors = On error_reporting = E_ALL upload_max_filesize = 2M post_max_size = 8M date.timezone = Europe/Amsterdam
Finding the php.ini that is used
There is not one php.ini but one per way of running PHP. On Debian
and Ubuntu each SAPI has its own folder:
/etc/php/8.4/cli/ for the command line,
/etc/php/8.4/fpm/ for PHP-FPM and
/etc/php/8.4/apache2/ for the Apache module. Next to
each php.ini is a conf.d/ folder whose
.ini files are read next, in alphabetical order — that
is where packages switch their extensions on. Ask PHP itself instead
of guessing:
php --ini
Configuration File (php.ini) Path: /etc/php/8.4/cli Loaded Configuration File: /etc/php/8.4/cli/php.ini Scan for additional .ini files in: /etc/php/8.4/cli/conf.d Additional .ini files parsed: /etc/php/8.4/cli/conf.d/10-opcache.ini, /etc/php/8.4/cli/conf.d/10-pdo.ini, /etc/php/8.4/cli/conf.d/20-mbstring.ini, …
This shows the command line's files only. For the web
server, a page that calls phpinfo() prints the same
information ("Loaded Configuration File") as an HTML table, along
with every setting and extension. PHP ships two templates to start
from: php.ini-development (shows errors) and
php.ini-production (hides errors from visitors and logs
them).
Common settings
| Setting | Built-in default | What it controls |
|---|---|---|
memory_limit |
128M |
Most memory one script may use; -1 means no
limit (Debian's CLI php.ini uses that)
|
max_execution_time |
30 (seconds) |
How long a web request may run; the CLI always uses 0, no limit |
display_errors |
on |
Show errors in the output; must be Off in
production (see
Errors and Exceptions)
|
error_reporting |
E_ALL |
Which kinds of errors are reported at all |
upload_max_filesize |
2M |
Largest single uploaded file |
post_max_size |
8M |
Largest whole request body; must be at least
upload_max_filesize
|
date.timezone |
empty (UTC) | Default time zone for date functions |
"Built-in default" is what PHP uses when php.ini does not set the
value; the php.ini files that distributions install often change
some of them. A typical ticket — "uploads over 2 MB fail" — is
usually upload_max_filesize or
post_max_size on the web server's php.ini, plus a
similar limit in the web server itself.
Changing settings
Settings are applied in layers; a later layer wins:
- Built-in defaults.
- php.ini, then the files in
conf.d/. -
Per-folder settings:
.user.inifiles with PHP-FPM, orphp_valuelines in.htaccesswith mod_php — common on shared hosting. -d name=valueon the command line.-
ini_set()in the script itself, for settings that may be changed at runtime.
<?php
echo "memory_limit: ", ini_get('memory_limit'), "\n";
echo "max_execution_time: ", ini_get('max_execution_time'), "\n";
$old = ini_set('memory_limit', '256M');
echo "memory_limit changed from $old to ", ini_get('memory_limit'), "\n";
var_dump(ini_set('upload_max_filesize', '50M')); // not allowed at runtime
var_dump(ini_get('no_such_setting'));
memory_limit: 128M max_execution_time: 0 memory_limit changed from 128M to 256M bool(false) bool(false)
ini_get() returns settings as strings.
ini_set() returns the old value, or
false when it is not allowed: the upload limit has to
be known before the script starts, because PHP has already received
the upload by then. These examples run without a php.ini, so they
show the built-in defaults. After editing php.ini for a website,
reload or restart
PHP-FPM (for example php8.4-fpm) or Apache; until then
the old values stay in effect.
Extensions
A PHP extension is a module,
usually written in C, that adds functions, classes and constants to
PHP. Some are compiled into every PHP (json,
pcre for regular expressions, date);
others are separate files that php.ini loads with an
extension= line: database drivers such as
pdo_mysql and mysqli,
mbstring for multibyte text, intl,
curl, gd for images. Applications list the
extensions they need; WordPress, for example, needs
mysqli.
<?php
foreach (['json', 'pcre', 'session', 'xdebug'] as $name) {
echo str_pad($name, 8), extension_loaded($name) ? "loaded" : "not loaded", "\n";
}
var_dump(function_exists('json_encode'));
json loaded pcre loaded session loaded xdebug not loaded bool(true)
A missing extension shows up as an error such as
Call to undefined function mb_strlen() or
Class "PDO" not found. On Debian and Ubuntu, each
extension is a package named after the PHP version:
php -m | grep -i mbstring # is it loaded on the command line?
sudo apt install php8.4-mbstring # install it for PHP 8.4
sudo systemctl restart php8.4-fpm # make the web side load it
The package drops an .ini file into the
conf.d/ folders, so the extension is enabled for every
SAPI. Debian's phpenmod and
phpdismod commands switch an installed extension on or
off. Some extensions, such as OPcache and Xdebug, are
Zend extensions that hook into the engine itself; they are
loaded with zend_extension= instead.