PHP in a nutshell: Variables and constants
Before we start
I’m assuming you already program in something else — JavaScript, Python, C#, Java, or maybe Go. You know what a variable, a function, a loop or a class is, and I’m not going to explain those concepts in this series. What I will show you is how the building blocks common to other programming languages look and work in PHP — and, above all, where PHP behaves differently from what your intuition or experience with another language suggests.
The whole course targets PHP 8.0–8.3. In every example I assume we’re using
declare(strict_types=1); at the top of the file, even when I don’t write it out. In a moment I’ll explain why that
declaration shouldn’t be optional in any of your files.
In this part we’ll go through the structure of a PHP file, strict typing mode, three functions that are handy for “peeking” at variable values, variables, references and constants. In other words, everything you need to get going with even the simplest PHP code.
The PHP file
Every PHP file starts with an opening tag. Without it, the interpreter treats the contents as plain text and sends it to the browser exactly as it is.
<?php // opening tagdeclare(strict_types=1);
echo 'Hello';
/* Thanks to the tag, the browser shows "Hello" — without it, we would see declare(strict_types=1); echo 'Hello';*/The closing tag: best left out
The ?> tag is optional, and in files that contain only PHP you should leave it out. This isn’t a matter of
taste — it protects you from one specific, exceptionally annoying bug.
<?phpdeclare(strict_types=1);
return ['db' => 'mysql'];?> ← this empty line gets sent to the browserEverything that ends up after ?> — a space, a tab, a newline — is sent by PHP to the browser as page
content. And together with the first byte of content goes the full set of HTTP headers, because PHP adds them
automatically. When you later try to set any of them — header(), setcookie(), session_start() — you get the
headers already sent error.
Omitting
?>is one of those habits worth building at the very beginning of your PHP journey. The PSR-12 standard explicitly requires it.
Tag types
| Tag | Status |
|---|---|
<?php ... ?> |
Standard, always available |
<?= $x ?> |
Short echo — always available since 5.4, safe in HTML templates |
<? ... ?> |
Short tag — depends on the short_open_tag directive in php.ini, avoid |
<% %>, <script language="php"> |
Removed in PHP 7 |
Use <?= only in templates, where you mix HTML with PHP:
<h1><?= htmlspecialchars($user->name) ?></h1>Comments and semicolons
// single-line# also single-line (shell style — works, but almost nobody uses it in PHP)
/* multi-line comment */
/** DocBlock — read by IDEs, documentation generators and reflection (more on that in later parts) */Every statement in PHP code ends with a semicolon. The only exception is the last statement before ?> — but since
we omit ?>, that exception should, as a rule, never come up.
declare(strict_types=1) — the most important line in the file
If you’re coming from a statically typed language, this is where PHP will surprise you the most. Type declarations in PHP look familiar:
function addItems(int $count): int{ return $count;}It looks like a contract. By default it isn’t one. Without strict mode PHP doesn’t reject the wrong type — it quietly converts it.
// without declare(strict_types=1)addItems('5'); // int(5) — string turned into a numberaddItems(5.9); // int(5) — TRUNCATED, no warningaddItems(true); // int(1)addItems('abc'); // TypeError — this one can't be converted anymoreLook at the second line. 5.9 becomes 5. No warning, no notice, no trace in the logs. If that number was a price,
a discount or a parcel weight — you’ve just lost data, and you’ll find out three months from now from a customer
complaint.
One line at the top of the file turns those conversions into errors:
<?phpdeclare(strict_types=1);
// now every one of these calls throws a TypeErroraddItems('5');addItems(5.9);addItems(true);| Conversion | Default mode | strict_types=1 |
|---|---|---|
'5' → int |
Cast to 5 |
TypeError |
5.9 → int |
Cast to 5 — silent data loss |
TypeError |
true → int |
Cast to 1 |
TypeError |
5 → float |
Widened to 5.0 |
Widened to 5.0 — always allowed |
| Covers | parameters, return types, assignments to typed properties | the same |
Widening int → float is the only exception, and it works in strict mode too. It’s lossless, so there’s no reason
to block it.
Three things you need to remember
First: this setting applies to a file, not to the project. There’s no global switch. It isn’t inherited through
require. Every new file starts with this line — no exceptions.
Second: it must be the first statement in the file. Only <?php and, optionally, a block-style namespace
declaration may come before it. Anything else is a fatal error.
Third — and this one is genuinely non-obvious — what matters is the file where the call is written, not the one where the function is declared.
<?php// a library without strict mode
function calculate(int $value): int{ return $value * 2;}And now the file that calls that function:
<?phpdeclare(strict_types=1);
require 'vendor/legacy/Calculator.php';calculate('10'); // TypeError — even though the library has no strict modeThis works in your favour: by enabling strict mode on your side, you also protect yourself from your own mistakes when calling external code. It doesn’t work the other way round — a library with strict mode won’t enforce anything on your file without it.
Strict mode applies to built-in functions as well.
strlen(123)passes by default, but understrict_types=1it throws aTypeError.
Printing and inspecting values
echo and print
echo 'a', 'b'; // accepts MULTIPLE arguments, returns nothingprint 'a'; // returns int(1), so it can be used in an expressionIn practice, use echo. print only makes sense when you need the return value — for example inside a conditional
expression — which is an extremely rare scenario.
Neither of them is suitable for checking what a variable contains or what type of data is assigned to it:
$arr = ['a' => 1];echo $arr; // "Array" + warning "Array to string conversion"Three functions for inspecting values
$v = ['a' => 1, 'b' => [2]];
print_r($v); // human-readable, WITHOUT types// Array// (// [a] => 1// [b] => Array// (// [0] => 2// )// )
var_dump($v); // type + value + string length + array size// array(2) {// ["a"]=>// int(1)// ["b"]=>// array(1) {// [0]=>// int(2)// }// }
var_export($v); // valid PHP CODE that you can paste back into a file// array (// 'a' => 1,// 'b' =>// array (// 0 => 2,// ),// )On arrays the difference looks cosmetic. On scalar values it becomes dramatic — only var_dump distinguishes
types:
| Value | print_r |
var_dump |
var_export |
|---|---|---|---|
'hi' |
hi |
string(2) "hi" |
'hi' |
1.0 |
1 |
float(1) |
1.0 |
null |
(nothing) | NULL |
NULL |
true |
1 |
bool(true) |
true |
false |
(nothing) | bool(false) |
false |
print_r(false)andprint_r(null)print nothing at all. In this case an empty screen doesn’t mean something didn’t work — it means the variable’s value is falsy. That fact has given more than one beginner a headache.
When to use which:
| Shows types | Returns PHP code | Can return the result instead of printing it | |
|---|---|---|---|
print_r |
❌ | ❌ | print_r($v, true) |
var_dump |
✅ | ❌ | ❌ (needs output buffering) |
var_export |
partially | ✅ | var_export($v, true) |
The last column tells you whether the function can return the output as a plain string instead of printing it to the screen right away. That comes in handy when you want to write it to a log rather than show it to the user.
A practical rule: var_dump() when you’re debugging a value you don’t trust, var_export() when you want to turn a
value into a test fixture, print_r() when you just want a quick look at the shape of an array.
Variables
$name = 'Paweł'; // names start with $ and are case-SENSITIVE$Name = 'someone else'; // this is a different variableVariables aren’t declared separately — in PHP, declaration is inseparable from assigning a value. The first
assignment creates the variable, and there’s no equivalent of JavaScript’s let foo;, which creates an undefined
variable you can assign a value to later.
Variable names
After the $ sign, the name must start with a letter or an underscore, followed by letters, digits and underscores.
A digit in the first position is a syntax error.
$name; $_internal; $user2; // ✅$2fast; // ❌ syntax errorExactly one name is reserved: $this. Inside a class it points to the current object, and trying to assign
anything to it is a fatal error.
Variable variables
PHP lets you use the value of one variable as the name of another:
$name = 'Paweł';$varName = 'name';
echo $$varName; // 'Paweł' — the same as $name$$varName = 'new'; // sets $name = 'new'Inside string interpolation you have to delimit it unambiguously with braces:
echo "{$$varName}"; // correctecho "${$varName}"; // deprecated since 8.2, removed in PHP 9I’m mentioning this because you’ll run into both forms in old code — not because you should use them. Variable variables break static analysis, autocompletion and refactoring: neither your IDE nor any other tool can guess which name will only come into existence at runtime.
If you’re tempted by this construct, what you almost always have is data that’s better kept in an array:
$values = ['name' => 'Paweł'];
echo $values['name']; // instead of echo $$varName$values['name'] = 'new'; // instead of $$varName = 'new'The gain is concrete: key names are visible in the code, so you can search for them, rename them and check them.
References
By default PHP assigns by value. Objects are the exception, but that’s a topic for part eight — for now let’s stick to scalars (that is, single values: numbers, strings, booleans).
$a = 1;$b = $a; // copy of the VALUE — $b gets its own place in memory$c = &$a; // REFERENCE assignment — $c points to the same memory address as $a
$a = 2;
var_dump($b); // int(1) — the copy lives its own lifevar_dump($c); // int(2) — same address as $aunset() on such a variable detaches it from that address, but doesn’t remove the value itself from memory — as
long as any other variable points to it, the value lives on:
unset($c); // $c no longer exists, but $a still points to that address and holds 2A reference in PHP is not the same thing as a pointer in C. It isn’t a value — you can’t do arithmetic on it, you can’t store it in an array “for later”, and you can’t make a reference to a reference. It’s simply a second name for the same memory address, and that’s all there is to it. In practice you’ll need it less often than you’d think, and the easiest place to trip over it is a
foreachloop — we’ll come back to that in part four.
Constants
PHP has two ways of defining constants, and they are not interchangeable.
define('APP_NAME', 'deftcode'); // a function call — runs at runtimeconst APP_VERSION = '1.0'; // resolved at compile timeThe difference comes down to the moment the constant is created. const takes a constant expression and can’t be
conditional or computed on the fly. define() is an ordinary function call, so you can wrap it in an if or base it
on a variable:
if ($env === 'dev') { define('DEBUG', true); // ✅ works}
if ($env === 'dev') { const DEBUG = true; // ❌ error — const can't be conditional}define() |
const |
|
|---|---|---|
| Namespace | ignores the current one — the constant belongs to the global namespace | goes into the current namespace |
| Conditional definition | ✅ | ❌ |
| Speed | slower (runtime) | faster (compile time) |
| Inside a class | ❌ | ✅ |
| Case-insensitive option | removed in PHP 8 | not applicable |
The first row needs a word of explanation. A namespace is a prefix attached to the names from a given file — roughly what a directory is for files. It lets you have two classes or constants with the same name in one project. You declare it at the top of the file, and from that point on it applies to everything below:
namespace App;
const VERSION = '1.0'; // creates App\VERSIONdefine('NAME', 'deftcode'); // creates NAME — in the global namespace
echo VERSION; // '1.0' — PHP finds App\VERSIONecho NAME; // 'deftcode' — App\NAME doesn't exist, so PHP falls back to the global oneThe difference only becomes visible in another file, because it decides how the constant has to be imported:
namespace App\Model;
use const App\VERSION;
echo VERSION; // '1.0' — works thanks to the import aboveecho \App\VERSION; // '1.0' — the same, just with the full pathecho NAME; // 'deftcode' — global, so no import is neededWhen reading a constant, PHP starts with the current namespace, and if it doesn’t find it there, it checks the global
one — which is why NAME works everywhere without an import. That convenience has a price, though: define() creates
a name visible across the whole project, so two libraries wanting the same name will get in each other’s way. const
stays in its own namespace and travels together with the rest of the code.
Arrays as constants are allowed in both forms:
const ROLES = ['admin', 'editor']; // since PHP 5.6define('LIMITS', ['api' => 100]); // since PHP 7.0And two helper functions that come in handy with configuration:
defined('DEBUG'); // bool — whether the constant existsconstant('APP_NAME'); // read by a name given as a stringIn practice: use const in application code (inside classes and namespaces), and leave define() for bootstrap
files, where something genuinely depends on the environment.
Magic constants
These aren’t real constants — the interpreter replaces them with a value at compile time, so the same name gives a
different result depending on where it’s used. All of them return a string, except __LINE__, which returns a number.
| Constant | What it returns | Example value |
|---|---|---|
__LINE__ |
the line number it appears on | 42 |
__FILE__ |
the full path to the file, including its name | /app/src/Order.php |
__DIR__ |
the file’s directory, without a trailing slash | /app/src |
__FUNCTION__ |
the name of the function or method it appears in | getName |
__CLASS__ |
the class name together with its namespace | App\Model\User |
__METHOD__ |
the method name prefixed with the class name | App\Model\User::getName |
__NAMESPACE__ |
the current namespace | App\Model |
__DIR__ is the one you’ll use most often — for building paths that don’t depend on the working directory:
require __DIR__ . '/../vendor/autoload.php';::class instead of strings with class names
::class is worth remembering separately. It isn’t a magic constant from the table above, but it solves a related
problem — it returns the fully qualified class name as a string:
use App\Model\User;
echo User::class; // 'App\Model\User'echo $user::class; // the same, on an instance (since PHP 8.0)Why it matters: 'App\Model\User' typed by hand is just text. Your IDE won’t refactor it, static analysis won’t check
it, and a typo only shows up at runtime.
User::class works differently, because it relies on the use import at the top of the file. When you move the class
to another namespace or rename it, the IDE fixes that one import, and User::class starts returning the new name on
its own. A hand-typed string stays exactly as it was — and points to a class that no longer exists.
That leaves the question of why you’d pass a class name at all, since you usually work with ready-made objects. It’s useful when what you’re passing along isn’t the object itself, but information about which class something should create or be compared against:
$class = User::class;$user = new $class(); // the object is only created here
if ($someObject::class === User::class) { // this object is of exactly the User class}What’s next
You now have a file, strict mode, tools for inspecting what’s inside variables, and ways of storing data in variables and constants. The most important thing is still missing: what actually lives inside those variables.
In the next part we’ll look at PHP’s type system — why 'abc' == 0 only stopped being true in PHP 8,
why (bool) '0' is false while (bool) '0.0' is true, and which of these things you genuinely need to learn
by heart.