A PHP program is a sequence of statements; expressions produce values, while conditions, loops, and functions determine what happens with those values.
Automatic conversion, loose comparison, and the low precedence of and and or can make an innocent-looking condition behave unexpectedly.
Enable strict types at file entry points, prefer ===, split complex expressions into named steps, and declare function parameter and return types.
What it is and why it exists
PHP fundamentals are the rules that turn source text into behavior: file boundaries, statements, expressions, variables, operators, control structures, and functions. A PHP file can be a command-line script or code handed to the runtime by a web server. In either case, the runtime parses the code and begins executing at the top level.
A statement is one complete unit of execution, such as an assignment, a function call, or return. An expression produces a value, such as $price * $quantity, $user ?? 'guest', or a function call. Assignment is itself an expression, so it changes a binding and also yields the assigned value.
These rules solve two problems. A program needs to combine and transform data. It also needs to choose a path from a result, repeat work, and name behavior for reuse. You encounter these structures in controllers, CLI commands, template entry points, queue jobs, and tests.
“Fundamentals” should not compress the whole language into a syntax catalog. Array shapes, conversion rules, the object model, and exception handling have their own topics. This page follows a narrower thread: how PHP executes a series of operations and how you keep those operations understandable.
How it works
Files, tags, and statements
A PHP-only file starts with <?php and normally omits the closing ?>. That prevents whitespace after the closing tag from leaking into a response. A template containing HTML can leave and re-enter PHP mode, but application classes, configuration, and CLI scripts generally need only one opening tag.
Most simple statements end with a semicolon. Braces group several statements under a structure such as if, foreach, or a function. They organize control flow, but unlike some languages, ordinary braces do not create a new block scope for variables.
Top-level statements run when execution reaches them. A function declaration defines callable behavior, while its body runs only when the function is called. Inside a function, return ends that call and hands back a value; directly inside an included file, it ends that file and returns a value to the including code.
declare(strict_types=1); must be the first statement in the file, after the opening tag. It tightens scalar conversion for user-defined function calls made by that file. It is not an input validator and does not change how == compares values.
Names, values, and assignment
Variable names begin with $ and are case-sensitive. Assignment evaluates the right side and binds the result to the name on the left; a later assignment can bind a value of another type. Runtime types belong to values rather than permanently to variable names. The details are covered in php/data-types.
The exact semantics of ordinary assignment depend on the kind of value. Scalars and arrays behave as values, so changing an assigned copy does not change the original variable. An object variable holds a handle to an object. The reference operator & creates variable aliases and is not another spelling for ordinary object assignment.
Variables have scope. An ordinary variable created inside a function belongs to that function, and an outer variable with the same name does not enter automatically. global can connect to a global name, but it hides a dependency. Passing the value as a parameter and returning the result is usually clearer.
Variable names and language keywords follow different rules. $order and $Order are different variables, while if, IF, and If are recognized as the same keyword. Team code should still use consistent casing rather than treating the flexibility of keywords or function names as a naming technique.
A name that should not be rebound while the program runs can be declared as a constant. File- or namespace-level constants can use const TAX_RATE = 0.2;, while define() creates a constant at runtime. Constants fit genuinely stable program configuration; deployment settings, secrets, and per-request values should not be hard-coded as constants.
Strings can use single or double quotes. Double-quoted strings process variable interpolation and escape sequences such as \n; single-quoted strings perform limited escaping for backslashes and single quotes. When interpolation becomes complicated, compute the values first and format them with sprintf() instead of piling braces into a string.
Expressions and operators
PHP uses operator precedence to group an expression before evaluating the groups. Multiplication binds more tightly than addition, so 2 + 3 * 4 equals 14. When the business rule matters more than the syntax rule, parentheses state it directly, as in ($subtotal + $shipping) * $taxRate.
It is more useful to learn common operators by purpose than to memorize every symbol at once.
| Purpose | Operators | Result |
|---|---|---|
| Arithmetic | +, -, *, /, %, ** | A number |
| String concatenation | . | A string |
| Comparison | ===, !==, <, >=, <=> | A Boolean or comparison integer |
| Logic | &&, ` | |
| Default value | ?? | The left value when set and not null; otherwise the right value |
| Conditional value | condition ? trueValue : falseValue | The selected branch value |
=== compares both type and value, while == first applies loose-comparison rules. External input often enters a program as strings, so $input === 0 and $input == 0 answer different questions. Validate and normalize input to its target type before comparing it strictly.
&& and || perform short-circuit evaluation . When the left side determines the result, the right side does not run. This is useful for checking a precondition before a later call, but code becomes fragile when the right side hides a write that must occur.
Output and string boundaries
PHP concatenates strings with ., not +. The plus operator enters numeric arithmetic rules, so copying string concatenation from another language can produce a type error or a completely different number. Interpolation is fine for a short double-quoted string; printf() or sprintf() is clearer when several fields need formatting.
echo is a language construct and can output one or more strings directly. print also outputs a string and returns the integer 1, though that return value rarely has business meaning. When output needs fixed decimal places, field widths, or another format, a format function keeps the requirement in one format string.
Producing a value does not mean safely generating HTML, JSON, SQL, or a shell command. Each target format has its own encoding rules; one general idea of “escaping special characters” cannot be reused across contexts. The language produces the string, but the output boundary still needs the encoder or parameterized API for that context.
On the CLI, PHP_EOL is the platform line ending, while \n in a double-quoted string is a newline character. If a protocol explicitly requires LF, use "\n"; human-readable output for the current terminal can use PHP_EOL. They are not merely stylistic synonyms.
Control structures
if, elseif, and else choose paths based on a condition. Conditions are converted to Boolean, so an empty string, the string '0', integer 0, an empty array, null, and false all take the false branch. If those values mean different things in the domain, compare them explicitly instead of relying only on truthiness.
match selects and returns a branch value from one subject value. It uses strict comparison, never falls through to the next arm, and can handle unlisted values with default. switch remains common in existing code, but it uses loose comparison and each nonempty case usually needs an explicit break.
while checks its condition before every iteration, do ... while runs at least once, and for puts initialization, condition, and step in one place. foreach walks an array or Traversable value directly and is usually clearer than managing an index by hand. A loop body must allow its termination condition to occur, or the job will keep occupying the process.
break exits the current loop or switch; continue skips the rest of the current iteration and begins the next one. Nested structures accept a numeric argument to exit several levels, but that form is difficult to scan. Extracting the inner behavior into a function and returning early usually makes the exit path more obvious.
Choose a control structure from the question the code needs to answer.
| Question | Preferred structure | Exit |
|---|---|---|
| Run when a condition holds? | if | End of branch |
| Which result corresponds to one value? | match | Expression produces result |
| Continue while a condition is true? | while | False condition or break |
| Must the work run at least once? | do ... while | False condition or break |
| Is a counter still in range? | for | False condition or break |
| Does a container have another item? | foreach | End of iteration or break |
The table is not a replacement for judgment. A complicated for can become a while, and a match returning Booleans can become an if chain. Pick the form that exposes the termination condition, branch order, and result type most clearly.
Every structure needs boundary coverage. Test a loop with empty input, one item, and an input that triggers its exit condition; test every explicit branch and the default path. Running only the most common input leaves misordered conditions and unreachable branches in place.
Function boundaries
A function puts a group of statements behind a named call boundary. Parameters receive values from the caller, and return hands a result back. A function that executes no return, or only an empty return;, produces null.
Parameters can have defaults and type declarations, and a function can declare its return type. Required parameters should come before optional ones. Callers can pass by position or, in PHP 8, by name, but a public parameter name then becomes part of the calling contract.
Arguments are passed by value by default. Only a parameter explicitly marked with & can rebind the caller’s variable through that parameter. Passing by reference broadens a function’s side effects and belongs only in an interface that clearly promises in-place modification.
Smaller functions are easier to inspect, but “small” is not a mechanical line count. A function should do one accurately named job, put its input requirements at the boundary, and keep one stable meaning for its normal return value. Error paths belong to the mechanisms described in php/error-handling and php/exceptions.
Entry points and failure boundaries
A CLI script usually receives string arguments through $argv and writes text to standard output or standard error. A web request exposes query parameters, forms, cookies, files, and headers through the server interface. The language syntax is the same for both sources, and every external value still needs parsing and validation.
Superglobals such as $_GET, $_POST, and $_SERVER are directly available inside a function, but that does not make them good hidden dependencies. An entry point should read the fields it needs, convert them to explicit arguments, and then call business functions that do not depend on request state.
When the same business function takes ordinary values and returns an ordinary value, it can serve the web, CLI, and tests. The entry point owns protocol details and the business function owns the rule. That boundary also clarifies where a failure becomes a response or an exit code.
A parse error prevents the affected code from starting normally. Runtime failures can include TypeError, ValueError, division errors, and application exceptions. In PHP 8, many such failures implement Throwable, but not every diagnostic message automatically becomes an exception.
Fundamental code should make its normal data flow clear and handle failure at a boundary that owns a recovery policy. When a local function cannot recover, it should not return a false value that can be confused with a valid result. Diagnostic levels, exception chains, and production logging policy are covered in the error-handling topics.
Exit codes are also part of a CLI contract. Success normally returns 0; failure uses a nonzero value and writes a human-readable message to standard error. If a script prints “failed” but still exits with 0, an automated caller can treat it as a success.
Examples
The four examples follow one path: calculate values, choose an order state, aggregate stock in a loop, and finally move repeated rules into functions. Every output shown here was produced by the local PHP 8.3.33 CLI.
Combine expressions to calculate an order total
The first script uses arithmetic, logic, and a conditional expression for a member order. Each intermediate result has a name, so you can inspect every step separately.
<?php
declare(strict_types=1);
$unitPrice = 19.90;
$quantity = 3;
$isMember = true;
$subtotal = $unitPrice * $quantity;
$discountRate = $isMember && $subtotal >= 50.0 ? 0.10 : 0.0;
$total = $subtotal * (1 - $discountRate);
printf("subtotal=%.2f\n", $subtotal);
printf("discount=%.0f%%\n", $discountRate * 100);
printf("total=%.2f\n", $total);subtotal=59.70
discount=10%
total=53.73Multiplication completes before assignment, and comparison groups before && according to precedence. The discount expression could be compressed further, but retaining names for the subtotal and rate makes the money rule easier to test.
The example uses printf() to control display precision. It does not suggest that binary floating point fits every monetary calculation. A real settlement system normally stores an integer count of the smallest currency unit or chooses and tests a decimal fixed-point scheme.
Classify orders with control flow
The second script walks the orders and uses match (true) to select a status by priority. Branches run from the condition needing the most attention to the default state.
<?php
declare(strict_types=1);
$orders = [
['id' => 'A-100', 'paid' => false, 'total' => 80],
['id' => 'A-101', 'paid' => true, 'total' => 140],
['id' => 'A-102', 'paid' => true, 'total' => 35],
];
foreach ($orders as $order) {
$status = match (true) {
!$order['paid'] => 'payment-pending',
$order['total'] >= 100 => 'priority',
default => 'ready',
};
printf("%s: %s\n", $order['id'], $status);
}A-100: payment-pending
A-101: priority
A-102: readyEvery match (true) arm produces a Boolean and compares it strictly with the subject true. The unpaid check must precede the high-value check, or a large unpaid order would enter the priority arm too early.
The order data stays small because it exists only to show the control flow. Production code should validate keys and values at the input boundary before handing a stable shape to the classification rule; see php/arrays for array-shape details.
Skip out-of-stock items in a loop
The third script subtracts reserved quantities from the available stock for each SKU. continue ends an out-of-stock iteration immediately, leaving the main path to handle only shippable items.
<?php
declare(strict_types=1);
$stockRows = [
['sku' => 'BK-1', 'available' => 4, 'reserved' => 1],
['sku' => 'PN-2', 'available' => 2, 'reserved' => 2],
['sku' => 'NT-3', 'available' => 7, 'reserved' => 3],
];
$readyUnits = 0;
foreach ($stockRows as $row) {
$freeUnits = $row['available'] - $row['reserved'];
if ($freeUnits <= 0) {
printf("%s: skipped\n", $row['sku']);
continue;
}
$readyUnits += $freeUnits;
printf("%s: %d ready\n", $row['sku'], $freeUnits);
}
printf("total: %d ready\n", $readyUnits);BK-1: 3 ready
PN-2: skipped
NT-3: 4 ready
total: 7 readyThe loop calculates the local result for an iteration before deciding whether to skip or add it. $readyUnits changes in one place, avoiding the missed and duplicate additions that arise when several branches update the total independently.
This loop does not use references because it only reads the stock rows. Code that needs updated results can explicitly build a new array. If it chooses in-place modification with &, it also has to handle the lingering-reference pitfall.
Move shipping rules into functions
The fourth script separates fee calculation from formatting. Parameter and return types make both function boundaries visible, while named arguments explain the role of each string at the call site.
<?php
declare(strict_types=1);
function shippingFee(float $subtotal, string $zone): float
{
if ($subtotal >= 100.0) {
return 0.0;
}
return match ($zone) {
'local' => 5.0,
'regional' => 9.5,
default => 15.0,
};
}
function orderSummary(string $orderId, float $subtotal, string $zone): string
{
$fee = shippingFee($subtotal, $zone);
return sprintf('%s subtotal=%.2f shipping=%.2f', $orderId, $subtotal, $fee);
}
echo orderSummary(orderId: 'A-103', subtotal: 72.5, zone: 'regional'), PHP_EOL;
echo orderSummary(orderId: 'A-104', subtotal: 120.0, zone: 'local'), PHP_EOL;A-103 subtotal=72.50 shipping=9.50
A-104 subtotal=120.00 shipping=0.00The free-shipping condition returns early, so the later match handles only orders that still need a charge. Both functions return stable types and do not pass results through global variables or reference parameters.
Named arguments make the call readable, but they also expose orderId, subtotal, and zone as external names. Renaming a public parameter can break callers, so an internal API that does not need that coupling can keep using positional arguments.
Pitfalls
Fix: use === when testing a value and put assignment in its own statement. If a condition genuinely needs to receive a function result, use explicit parentheses and a name that states the intent, then test both the true and false paths.
Fix: prefer && and || for Boolean conditions and add parentheses when mixing assignment, comparison, and logic. Do not guess grouping from the English reading. Check the operator-precedence table when it is unclear.
Fix: check whether the key exists, validate the accepted raw representations, and only then convert to a domain value. Business code should receive the normalized type and use === for states that must stay distinct.
Fix: explicitly end every case that should not fall through, or use match when the structure should return one value. Before migrating, test numeric strings, integers, and null, because strict matching can change legacy behavior.
Fix: iterate by reference only for intentional in-place modification, and immediately run unset($item) after the loop. The easier-to-review option is to build a new array or put the item transformation in a function that returns a new value.
Evaluation order and side effects
Operator precedence describes how an expression is grouped, and associativity describes how operators at the same precedence nest. Neither one defines the order in which subexpressions run. PHP generally leaves the evaluation order of subexpressions unspecified, so code cannot rely on an order observed in one run.
For example, do not read $index and change it with $index++ in the same expression while expecting an array access to use either the old or new value. Do not put two calls that must happen in sequence into one arithmetic expression. Split each side effect into its own statement and statement order makes the sequence explicit.
Short-circuit operators are a defined exception. $user !== null && $user->isActive() calls the method only when the left side is true; $cached ?? loadValue() calls the fallback only when the left side is absent or null. This conditional execution belongs to the operator contract and is safe to rely on.
A conditional expression also evaluates only its selected branch. It is useful for choosing between two side-effect-free values. When either branch logs, writes, or makes a complex call, an ordinary if makes the behavior easier to see. Readability is tied to correctness here because a reviewer needs to see which operations always run and which can be skipped.
An assignment expression returns the assigned value, so chained assignments and assignments inside conditions are valid syntax. This occasionally makes a parsing loop compact, but it also lets a mistyped = in place of === survive syntax checking. A code generator using this form should explain where assignment occurs and supply a test that executes the failure branch.
Function arguments can contain side effects too. Do not make correctness depend on which of several argument expressions runs first. Evaluate them into separate named values before the call. Named arguments change parameter correspondence and should not be treated as a tool for changing evaluation order.
File inclusion and scope
include and require evaluate another PHP file. Ordinary top-level code in that file runs when inclusion occurs, so inclusion is not a static operation that merely reads declarations. Including a file with side effects twice can register handlers, write output, or run initialization twice.
The inclusion point affects variable scope in the included code. An include executed inside a function lets the included file see local variables available at that point, and ordinary variables it creates remain in the same function scope. This implicit sharing makes a file hard to understand alone. Prefer explicit input data and an explicit return value.
An included file can execute return, ending its own processing and handing a value to the include expression. A successful include without an explicit returned value normally yields 1. A configuration file designed to return an array is easier to test than one that silently modifies several outside variables.
When include cannot find a file, it raises a warning and the current script may continue. A failed require stops that execution path. Use include only when the absent file has a deliberate fallback; definitions and configuration required at program startup normally have require semantics.
include_once and require_once avoid including the same file again, but they do not fix ambiguous ownership of initialization. Modern applications generally let Composer autoload classes instead of scattering manual paths through business functions. Autoloading locates class files; configuration and startup code with side effects still need a clear entry point.
A file path must not come directly from untrusted input. Concatenating a request parameter into include turns a data choice into a code or file choice and can permit arbitrary local file reads or execution. Select allowed modules through a fixed map and reject every value outside it.
Inclusion explains how many older PHP projects execute, and why top-level side effects are difficult to test. New entry-point files can stay limited to assembly: load the autoloader and configuration, create dependencies, and call one explicit application function. That keeps the start of execution and the values entering business logic visible in a small amount of code.
Further reading
4 questions · 2 predict-the-output · 1 spot-the-bug