Start Here

Interactive Shell

PHP can execute code interactively or run a short snippet without creating a script file. These tools are useful when you need to inspect one runtime value, verify a standard-library return type, reproduce an edge case, or explore the smallest version of a helper before adding it to a project.

The shell is an investigation tool, not a home for application behaviour. Anything that must be repeated, reviewed, deployed, or trusted belongs in a file with tests.

Start The Native Shell With php -a

Run:

php -a

A working interactive shell prints a prompt similar to this:

Interactive shell

php > echo PHP_VERSION . PHP_EOL;
8.5.6
php > var_dump(extension_loaded('mbstring'));
bool(true)

Do not type <?php at this prompt. The shell is already interpreting PHP code, and each complete statement still needs its semicolon.

The native shell depends on readline support. PHP's manual states that php -a provides the interactive shell when PHP was built with readline support; on Windows, the readline extension can provide it. Since PHP 8.1, php -a fails when readline is unavailable instead of falling back to the older, confusing interactive mode.

Check the local build rather than assuming the shell exists:

php -m
php --ri readline
php -a

php --ri readline reports extension information when readline is loaded. If it reports that the extension is absent, use php -r, a temporary script, or a project REPL instead.

Work With A Stateful Session

Variables, functions, classes, and included files remain available until the process ends:

php > $prices = [12.50, 8.25, 3.00];
php > echo array_sum($prices) . PHP_EOL;
23.75
php > function addTax(float $amount): float { return $amount * 1.2; }
php > var_dump(addTax(10.0));
float(12)

This persistent state is convenient, but it can also mislead you. A result may depend on a variable changed ten commands earlier. Restart the shell when you need a clean experiment. A function or class cannot simply be redefined under the same name in one process, so restarting is often easier than trying to repair a long session.

The prompt changes while PHP is waiting for a closing brace, parenthesis, quote, or comment. Complete the construct before expecting execution. Readline-enabled builds also provide command history and tab completion for names such as functions, constants, classes, variables, static methods, and class constants.

Use explicit inspection functions because the native shell is not designed to present every expression as a polished result:

  • var_dump() shows type and value;
  • var_export() prints a PHP representation;
  • print_r() is convenient for a quick array view but gives less type detail;
  • json_encode() is useful when the intended boundary is JSON.

Run One-Off Code With php -r

php -r executes the supplied code and exits:

php -r "echo PHP_VERSION . PHP_EOL;"
php -r "var_dump(extension_loaded('intl'));"
php -r "echo ini_get('memory_limit') . PHP_EOL;"

Do not include <?php or ?> in a -r snippet. The CLI option expects code without script tags and those tags cause a parse error.

php -r is especially useful in setup instructions and diagnostic commands because it is non-interactive. It can be run by a person, CI job, or support script and produces a fresh process each time. That fresh process prevents hidden state from an earlier experiment.

The surrounding shell parses quoting before PHP sees the code. Double quotes, single quotes, dollar signs, backslashes, and command substitution behave differently across POSIX shells, PowerShell, and Command Prompt. Keep published one-liners short. Once escaping becomes harder to understand than the PHP itself, put the code in a .php file.

Inspect The Exact CLI Runtime

An interactive result describes the CLI process that produced it. It does not automatically describe PHP-FPM, Apache's PHP module, a container, a queue worker, or another PHP binary on the machine.

Useful checks include:

php -r "echo PHP_BINARY . PHP_EOL;"
php -r "echo PHP_SAPI . PHP_EOL;"
php -r "var_dump(php_ini_loaded_file());"
php -r "var_dump(php_ini_scanned_files());"
php -r "var_dump(getcwd());"

php_ini_loaded_file() returns the loaded main configuration path or false. php_ini_scanned_files() reports additional parsed INI files or false. The distinction matters because extension configuration is often split into a scan directory instead of written directly in the main file.

Compare a normal process with controlled alternatives:

php -n -r "var_dump(php_ini_loaded_file());"
php -d memory_limit=256M -r "echo ini_get('memory_limit') . PHP_EOL;"

-n starts PHP without a php.ini. -d changes one configuration directive for that invocation. These commands are diagnostic controls: if behaviour changes under -n, configuration or an extension may be involved. They are not instructions to disable production configuration.

Load Project Code Deliberately

The native shell does not automatically know about Composer packages or your application classes. From a project root, you can load Composer's generated autoloader:

php > require getcwd() . '/vendor/autoload.php';
php > var_dump(class_exists(Composer\InstalledVersions::class));
bool(true)

Requiring vendor/autoload.php makes package and project classes discoverable according to the project's Composer configuration. It does not necessarily boot the framework, create a database connection, or load application environment files. Those steps depend on the project.

Be precise about the working directory. getcwd() reflects where the shell was launched, while __DIR__ entered directly in a shell expression does not represent an application source file. If a require path fails, inspect getcwd() and use an explicit path rather than guessing.

Use The Shell To Answer One Question

A useful experiment starts with a narrow question and an observable result. For example:

php -r "var_dump(filter_var('ada@example.com', FILTER_VALIDATE_EMAIL));"
php -r "var_dump(json_decode('{\"active\":true}', true, flags: JSON_THROW_ON_ERROR));"
php -r "echo (new DateTimeImmutable('2026-06-10'))->modify('+30 days')->format('Y-m-d') . PHP_EOL;"

The first command checks a return value and type. The second checks the array shape produced by JSON decoding. The third checks a date transformation from a fixed input, making the result reproducible.

Avoid experiments based on now, tomorrow, the machine timezone, random values, or mutable external data unless those variables are the subject of the investigation. Fixed inputs make it easier to explain and repeat the finding.

Move A Prototype Into A File

Suppose you explore a tag-normalisation rule in the shell:

php > $tag = trim('  Release_Candidate  ');
php > $tag = strtolower($tag);
php > $tag = preg_replace('/[ _-]+/', '-', $tag);
php > var_dump($tag);
string(17) "release-candidate"

The experiment proves one path. It does not define what happens for an empty value, unsupported punctuation, repeated separators, or multiple examples. Durable code needs an explicit contract:

PHP example
<?php

declare(strict_types=1);

function normaliseTag(string $tag): string
{
    $tag = trim($tag);

    if ($tag === '' || preg_match('/^[A-Za-z0-9 _-]+$/D', $tag) !== 1) {
        throw new InvalidArgumentException('Tag contains unsupported characters.');
    }

    $tag = strtolower($tag);
    $normalised = preg_replace('/[ _-]+/', '-', $tag);

    if ($normalised === null) {
        throw new RuntimeException('Tag normalisation failed.');
    }

    $normalised = trim($normalised, '-');

    if ($normalised === '') {
        throw new InvalidArgumentException('Tag must contain a letter or digit.');
    }

    return $normalised;
}

The function deliberately documents an ASCII input contract. It does not pretend that strtolower() implements Unicode-aware language rules. A project that accepts international text needs a separately designed Unicode policy and appropriate extension support.

Protect Secrets And Production Data

Readline-enabled PHP normally stores shell history in ~/.php_history. Since PHP 8.4, PHP_HISTFILE can select another history path. Your operating-system shell may also save the original php -r command.

Do not type passwords, API tokens, private customer data, or production connection strings into either command history. Clearing a terminal screen does not remove history files, process arguments, audit logs, screen recordings, or copied output.

Treat application REPLs as production access when they boot production configuration. A read-looking expression can trigger lazy loading, accessors, events, or external calls. Database writes, queue dispatches, emails, payments, and destructive filesystem operations belong in reviewed, repeatable commands with safeguards and logs.

Know The Project-Specific Alternatives

Laravel Tinker is a REPL powered by PsySH and can boot a Laravel application. PsySH can also be used outside Laravel. These tools provide richer inspection than PHP's native shell, but loading the application also increases the possible side effects.

Symfony Console is primarily a framework for named commands, not an automatic project REPL. A Symfony project may add PsySH or another shell, but bin/console by itself should not be described as an interactive PHP evaluator.

For static facts, a shell may be unnecessary. The PHP CLI provides reflection-oriented options:

php --rf array_map
php --rc DateTimeImmutable
php --ri json

These inspect a function, class, or extension without creating session state.

Stop Exploring At The Right Time

Move the work into a committed script, test, migration, or console command when:

  • another person needs to repeat it;
  • the result changes data or external systems;
  • input validation and failure handling matter;
  • more than a few lines are required;
  • the finding documents a bug or compatibility decision;
  • code review or an audit trail is required.

A strong workflow is: state one question, run the smallest controlled experiment, record the result, then encode lasting behaviour in a test or named piece of code.

What You Should Be Able To Do

After this lesson, you should be able to determine whether php -a is available, use php -r without script tags, inspect the exact CLI configuration, load Composer's autoloader deliberately, avoid history leaks, and turn a successful experiment into validated project code.

The official references are PHP's interactive shell manual and CLI options manual.

Practice

Practice: Investigate Fresh CLI Processes

Use php -r to build a small evidence report about the PHP binary your terminal runs.

Task

Run separate one-line commands that report:

  • PHP_BINARY, PHP_VERSION, and PHP_SAPI
  • the result of php_ini_loaded_file() and php_ini_scanned_files()
  • whether mbstring and json are loaded
  • the current memory_limit

Then run controlled comparisons:

  • repeat the INI and extension checks with php -n -r
  • run the memory check with php -d memory_limit=192M -r

Requirements:

  • do not include <?php or ?> in any -r snippet
  • label values so the output remains understandable when pasted into a bug report
  • keep each command short enough that its shell quoting is readable
  • record the actual output from your machine rather than copying sample paths or versions
  • state which facts changed under -n and under -d
  • explain why a fresh php -r process is more reproducible than relying on state left in a long php -a session

Do not conclude that -n disables every extension. Distinguish extensions built into PHP from extensions enabled through configuration.

Show solution

These commands use PHP string literals inside the shell's outer double quotes, avoiding nested double-quote escaping.

php -r "echo 'binary=', PHP_BINARY, PHP_EOL, 'version=', PHP_VERSION, PHP_EOL, 'sapi=', PHP_SAPI, PHP_EOL;"
php -r "var_export(['loaded_ini' => php_ini_loaded_file(), 'scanned_ini' => php_ini_scanned_files()]); echo PHP_EOL;"
php -r "var_export(['mbstring' => extension_loaded('mbstring'), 'json' => extension_loaded('json')]); echo PHP_EOL;"
php -r "echo 'memory_limit=', ini_get('memory_limit'), PHP_EOL;"

Run the configuration-free comparisons:

php -n -r "var_export(['loaded_ini' => php_ini_loaded_file(), 'scanned_ini' => php_ini_scanned_files()]); echo PHP_EOL;"
php -n -r "var_export(['mbstring' => extension_loaded('mbstring'), 'json' => extension_loaded('json')]); echo PHP_EOL;"

Then override one directive for one process:

php -d memory_limit=192M -r "echo 'memory_limit=', ini_get('memory_limit'), PHP_EOL;"

A typical investigation finds that the normal process reports a main INI path and may report additional scanned files. Under -n, both configuration-file functions return false. A dynamically configured extension such as mbstring may disappear, while functionality compiled into the PHP binary remains available. That is why -n means "do not load php.ini," not "start PHP with no extensions."

The final command should print:

memory_limit=192M

That does not edit the machine's configuration. The -d value applies only to the PHP process launched by that command.

Each php -r invocation starts a new process with inputs visible in the command itself. A long php -a session can contain variables, functions, includes, and changed INI values from earlier experiments, so another person cannot reproduce a result from the final expression alone. Fresh processes remove that hidden session state.

Task: Build A CLI Runtime Fingerprint

Create runtime-report.php, a reusable diagnostic script that prints one JSON object describing the current PHP process.

Required Fields

The report must contain:

  • binary: PHP_BINARY
  • version: PHP_VERSION
  • sapi: PHP_SAPI
  • cwd: the current working directory or null when unavailable
  • loaded_ini: the main INI path or null
  • scanned_ini: the additional scanned INI string or null
  • memory_limit: the active directive value
  • timezone: the active default timezone
  • app_env: the APP_ENV value or null
  • extensions: an object containing boolean mbstring, intl, and pdo fields

Requirements:

  • use strict types
  • convert every false result to JSON null where the field contract requires it
  • encode with JSON_THROW_ON_ERROR, JSON_PRETTY_PRINT, and JSON_UNESCAPED_SLASHES
  • write only the JSON document and its final newline to STDOUT
  • do not hard-code any path, version, environment, extension result, or configuration value

Run the script normally, then run it with APP_ENV=local and memory_limit=192M supplied only for that invocation. Compare the reports and identify the fields that changed.

After the code, explain why this report describes one CLI process but does not prove that PHP-FPM or a web server uses the same binary, configuration, user, working directory, or environment.

Show solution

The script normalises PHP's false sentinel values to JSON null, so callers receive consistent JSON types instead of a mixture of path strings and booleans.

PHP example
<?php

declare(strict_types=1);

function stringOrNull(string|false $value): ?string
{
    return $value === false ? null : $value;
}

$report = [
    'binary' => PHP_BINARY,
    'version' => PHP_VERSION,
    'sapi' => PHP_SAPI,
    'cwd' => stringOrNull(getcwd()),
    'loaded_ini' => stringOrNull(php_ini_loaded_file()),
    'scanned_ini' => stringOrNull(php_ini_scanned_files()),
    'memory_limit' => stringOrNull(ini_get('memory_limit')),
    'timezone' => date_default_timezone_get(),
    'app_env' => stringOrNull(getenv('APP_ENV')),
    'extensions' => [
        'mbstring' => extension_loaded('mbstring'),
        'intl' => extension_loaded('intl'),
        'pdo' => extension_loaded('pdo'),
    ],
];

echo json_encode(
    $report,
    JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES,
) . PHP_EOL;

On a POSIX shell, run the controlled comparison with:

php runtime-report.php
APP_ENV=local php -d memory_limit=192M runtime-report.php

In PowerShell, set and remove the process environment variable explicitly:

php runtime-report.php
$env:APP_ENV = 'local'
php -d memory_limit=192M runtime-report.php
Remove-Item Env:APP_ENV

The second report should show "app_env": "local" and "memory_limit": "192M". Other fields should remain unchanged unless the command was run from a different directory or through a different PHP executable.

Every value is measured inside the process that prints the report. PHP-FPM may use another SAPI, binary, INI search path, operating-system user, environment policy, and working directory. To compare it accurately, expose equivalent diagnostics through a protected temporary web route or server-side log, remove that diagnostic afterward, and compare the fields rather than assuming the CLI report applies to the web runtime.

Task: Turn A Shell Prototype Into Tested Code

Prototype a tag-normalisation rule in php -a, then move the rule into a committed PHP function with executable checks.

Contract

normaliseTag() must:

  • accept ASCII letters, digits, spaces, underscores, and hyphens
  • trim leading and trailing whitespace
  • lowercase ASCII letters
  • replace each run of spaces, underscores, or hyphens with one hyphen
  • remove leading and trailing hyphens from the result
  • throw InvalidArgumentException for an empty input, unsupported characters, or a value that contains only separators
  • throw RuntimeException if preg_replace() reports an internal failure

First, show a short php -a transcript that transforms Release_Candidate one step at a time. The transcript is exploratory evidence, not the final implementation.

Then create a strict-types PHP script containing the function and table-driven checks for at least these valid cases:

  Release_Candidate   -> release-candidate
PHP 8 Basics          -> php-8-basics
one---two              -> one-two

Also verify that an empty string, ---, and release/v1 are rejected. The script must throw when any expectation fails and print All tag checks passed. only after every case succeeds.

After the code, explain why the function advertises an ASCII contract instead of claiming to normalise arbitrary human-language text.

Show solution
$ php -a
Interactive shell

php > $tag = trim('  Release_Candidate  ');
php > var_dump($tag);
string(17) "Release_Candidate"
php > $tag = strtolower($tag);
php > $tag = preg_replace('/[ _-]+/', '-', $tag);
php > var_dump($tag);
string(17) "release-candidate"

That result is only the happy path. The finished script defines rejection rules and checks several inputs:

PHP example
<?php

declare(strict_types=1);

function normaliseTag(string $tag): string
{
    $tag = trim($tag);

    if ($tag === '' || preg_match('/^[A-Za-z0-9 _-]+$/D', $tag) !== 1) {
        throw new InvalidArgumentException(
            'Tag must contain only ASCII letters, digits, spaces, underscores, and hyphens.',
        );
    }

    $tag = strtolower($tag);
    $normalised = preg_replace('/[ _-]+/', '-', $tag);

    if ($normalised === null) {
        throw new RuntimeException('Tag normalisation failed.');
    }

    $normalised = trim($normalised, '-');

    if ($normalised === '') {
        throw new InvalidArgumentException('Tag must contain a letter or digit.');
    }

    return $normalised;
}

$validCases = [
    '  Release_Candidate  ' => 'release-candidate',
    'PHP 8 Basics' => 'php-8-basics',
    'one---two' => 'one-two',
];

foreach ($validCases as $input => $expected) {
    $actual = normaliseTag($input);

    if ($actual !== $expected) {
        throw new RuntimeException(
            sprintf('Expected %s, received %s.', $expected, $actual),
        );
    }
}

$invalidCases = ['', '---', 'release/v1'];

foreach ($invalidCases as $input) {
    try {
        normaliseTag($input);
    } catch (InvalidArgumentException) {
        continue;
    }

    throw new RuntimeException(
        sprintf('Expected input %s to be rejected.', var_export($input, true)),
    );
}

echo 'All tag checks passed.' . PHP_EOL;

Expected output:

All tag checks passed.

The function names its ASCII contract because strtolower() does not implement every language's Unicode casing rules, and a URL or tag policy may need additional transliteration decisions. Claiming support for arbitrary human-language text would hide those product decisions. A project that needs international tags should define that policy explicitly and test it with representative data and the required extensions.