Types — What PHP Actually Stores
=== always (not ==). The string "0" is falsy (notorious trap). Cast with (int) at the boundaries. Use mb_* string functions for user input. Null coalescing ?? is your friend.The mental model
Quick mental model before we dive in. Types in any language are basically categories that tell the language what kind of data is in a variable. An int is a number. A string is text. A bool is true/false. The category determines what operations make sense — you can add two ints, but adding "hello" + "world" is a question (concatenate? error?). Different languages answer that question differently, and PHP has some... opinions.
Picture types like containers in a kitchen. A bowl holds soup, a glass holds water, a plate holds a sandwich. Technically you could put soup on a plate, but it wouldn't work great. Types tell PHP what kind of container each value is so it can warn you about nonsensical operations. Most of the time, PHP is loose about this. With strict types on (which we said you should do every time), it gets stricter at function boundaries.
The scalar types
"Scalar" just means "single value" — a number, a piece of text, a true/false. PHP has four scalar types:
$i = 42; // int
$f = 3.14; // float
$s = "hello"; // string
$b = true; // bool
There's no separate char type for single characters — those are just length-1 strings. There's no long, double, byte, or any of the machine-level numeric types you might know from C or Java. PHP picks an appropriate precision under the hood and you don't have to think about it.
One important caveat about floats that bites everyone exactly once: if you're dealing with money, don't use floats. Floats are stored in binary and can't precisely represent some decimal numbers — 0.1 + 0.2 equals 0.30000000000000004 in PHP (and basically every other language, this isn't PHP's fault). For money, either store cents as integers (so $5.99 becomes 599) or use the bcmath extension for arbitrary-precision arithmetic. Floats will lie to you about pennies and you don't want that in a finance app.
Heredoc and Nowdoc — multi-line strings
For longer text blocks — think email templates, HTML chunks, SQL queries — typing "line one\nline two\nline three" with embedded newlines gets ugly fast. PHP has a special syntax called heredoc:
$html = <<<HTML
<div class="card">
<h2>$title</h2>
<p>$body</p>
</div>
HTML;
The <<<HTML opens the block — that "HTML" is just a label you choose; you could write "CARD" or "TEMPLATE" or whatever — and the matching HTML; on its own line closes it. Important rule: the closing label has to be at the very start of its own line with no leading whitespace, otherwise PHP doesn't recognize it. This trips people up the first time.
Heredoc interpolates variables just like double quotes. There's also Nowdoc, which is the literal version (like single quotes):
$html = <<<'HTML'
This is literal $not_a_variable text.
HTML;
The single quotes around 'HTML' make it nowdoc. You'll see nowdoc all over this very tutorial app, actually — it stores long HTML chapter content (like the one you're reading) without escape-hell, since you never have to worry about a stray $ being interpreted as a variable.
Null — the explicit absence
There's one special type with exactly one value: null. It means "this variable holds nothing meaningful." Sounds simple but it has surprising depth, so let me hammer this one in.
Null is DIFFERENT from 0. Different from "". Different from false. Different from an empty array. They might all be "falsy" in conditions (more on that in a second), but they're not equal in strict comparisons. Null specifically means "the absence of a value" — like a missing column in a database, or a function returning "I couldn't find anything." Zero means "I have a value and it's zero." Empty string means "I have a value and it's an empty string." Different things.
null === 0 // false (different types)
null === "" // false
null === false // false
null === null // true (obviously)
Check for null with === null or is_null($x). Don't conflate "empty" with "null" — they're cousins, not the same.
Booleans and the falsy table
In conditions, PHP automatically converts values to bool. Most things are "truthy" (treated as true), but a specific list of values is "falsy" (treated as false). Memorize this list:
falseitself0and0.0""(empty string)"0"(the string "0" — yes, the literal string containing the character zero. This one's infamous.)[](empty array)null
The string "0" being falsy is the legendary PHP trap, and it's worth a real example. Picture this: you have a form where the user enters a number. They type "0" — a perfectly legitimate input. Your code does:
if ($input) {
// process it
}
...and it skips the processing because "0" is falsy. Your user is confused: "I typed something, why didn't it work?" The fix: when you actually mean "is the string empty," use $input === "" or strlen($input) === 0. Be explicit about what you actually care about. Save yourself the 2am debugging session — and at the same time, treat every form input with the suspicion it deserves.
=== vs == — the most important rule in PHP
Read this section carefully. It's one of the most important rules in the whole language.
PHP has two equality operators: == and ===. They are not the same.
==is "loose equality" — PHP coerces types before comparing. Often gives surprising results.===is "strict equality" — values must match in both value AND type. No coercion.
Here's a few examples of how == can mess with you:
"1" == 1 // true
[] == false // true
0 === "0" // false (different types)
"1" === 1 // false
The string "1" being equal to integer 1 with == kind of makes sense if you squint. But empty array being equal to false? That's just weird. Modern PHP (8.0+) cleaned a lot of the most egregious comparisons up, but the rule still applies: always use === unless you have a really specific reason. Same for inequality — use !== instead of !=. Rule, capitalized for emphasis: USE TRIPLE EQUALS. End of debate.
🐍 Python: Python's == is way safer than PHP's == — Python doesn't auto-coerce types nearly as aggressively. PHP === ≈ Python's normal ==. PHP == is closer to "controlled chaos." If you've been using Python's == happily for years, that's the equivalent of PHP's ===, not ==.
Arrays — briefly, here, because they deserve their own chapter
PHP arrays are one of the language's most-used and most-misunderstood features. The TL;DR: they're a list and a dict mashed into one type. Same syntax handles both:
// Indexed (zero-based, like a list)
$tags = ['php', 'web', 'lamp'];
echo $tags[0]; // "php"
// Associative (string keys, like a dict)
$user = ['name' => 'Eric', 'age' => 25];
echo $user['name']; // "Eric"
Under the hood it's all the same type. Indexed arrays just happen to use 0, 1, 2 as their automatic keys. We'll spend a whole chapter on arrays soon (chapter 4) so don't sweat the details here. For now: they're everywhere, they're cheap, they grow on assignment with $tags[] = 'new'.
Type juggling and casting
Even with declare(strict_types=1) on, PHP still does some automatic type conversion in certain contexts — string concatenation, arithmetic, comparison. Strict types only enforces typing at function boundaries, not within expressions. So you'll still see "5" + 2 work and return 7. That's juggling.
To be explicit and bulletproof, you can "cast" a value to a specific type with a prefix in parentheses:
$id = (int) $_GET['id']; // force to int
$name = (string) $value; // force to string
$flag = (bool) $value; // force to bool
Casting is a great habit at the boundaries of your program. Every value that comes from outside — query parameters, form data, database results, API responses — arrives as some type that PHP picked, often a string. Casting at the boundary to the type you actually want gives you a clean, known-typed value to work with from there on.
Mental model: think of casting as customs at an airport. Stuff coming in from outside needs to be processed before it enters your country (your program). Casting is the customs check — confirms what kind of data you actually have, in the format you actually want. After customs, the rest of your country (codebase) can assume things are well-typed.
Inspecting types when something is weird
When something isn't behaving the way you expected, you can interrogate values to see what type they actually are:
gettype($x); // returns "integer" / "string" / "array" / ...
is_int($x); // boolean - is it an int?
is_string($x);
is_array($x);
is_bool($x);
is_null($x);
var_dump($x); // prints type AND value — your debugging bestie
When in doubt, slap a var_dump() at the suspect spot in your code and refresh the page. Half of PHP debugging is just being surprised by what type something turns out to be, then realizing where the conversion happened. Trust the var_dump output, not your assumptions.
Null coalescing — the operator you'll use constantly
PHP 7+ added an operator specifically for the "this might not exist, fall back to a default" pattern. You'll reach for it every single time you handle $_GET, $_POST, or any other potentially-missing data:
$name = $_GET['name'] ?? 'guest';
Read that as: "set $name to $_GET['name'], OR if that doesn't exist or is null, use 'guest' as a default." Replaces this much wordier old-school pattern:
$name = isset($_GET['name']) ? $_GET['name'] : 'guest';
There's also ??= ("assign if currently null"), which is useful for lazy initialization:
$_SESSION['queue'] ??= []; // initialize to empty array if not set
You'll use ?? dozens of times per project. One of those small operators that punches above its weight.
🐍 Python: $_GET['name'] ?? 'guest' ≈ request.args.get('name', 'guest'). Same "default if missing" pattern in different clothes.
Build: Type Inspector — A Debug Tool You'll Actually Use
Let's build a real little tool. Type Inspector is a page where you can paste any value into a URL parameter and instantly see what PHP thinks it is — its type, how it casts, whether it's truthy, what strlen vs mb_strlen say about it. Not a contrived exercise — this is the kind of thing you'll genuinely keep around for debugging "wait, why is this value behaving weird?" moments.
Each row in the output uses a different inspection function on the same value, so you can see how PHP perceives the input from every angle. Spending five minutes paying attention to which rows return what for inputs like "0", "héllo", and empty string will teach you more about PHP's type system than any spec page.
- Create
/home/erictey/server/type-inspector.php. - Paste:
<?php declare(strict_types=1); function row(string $label, mixed $value): string { $type = gettype($value); $dump = print_r($value, true); return "<tr><td>$label</td><td>$type</td><td><code>" . htmlspecialchars($dump) . "</code></td></tr>"; } $raw = $_GET['v'] ?? ''; ?> <!DOCTYPE html> <html> <head> <style> body { background: #07050d; color: #f0e9ff; font-family: monospace; padding: 30px; } table { border-collapse: collapse; } td { border: 1px solid #ff2e8855; padding: 6px 12px; } th { color: #5bf0ff; text-align: left; padding: 6px 12px; } code { color: #5bf0ff; } input { background: #110a1c; border: 1px solid #ff2e88; color: #f0e9ff; padding: 4px 8px; font-family: monospace; } </style> </head> <body> <h1>Type Inspector</h1> <form> <input name="v" value="<?= htmlspecialchars($raw) ?>" placeholder="type anything"> <button>Inspect</button> </form> <table> <tr><th>Label</th><th>Type</th><th>Value</th></tr> <?= row("Raw \$_GET['v']", $raw) ?> <?= row("(int) cast", (int) $raw) ?> <?= row("(float) cast", (float) $raw) ?> <?= row("(bool) cast", (bool) $raw) ?> <?= row("=== '0'?", $raw === '0') ?> <?= row("== 0?", $raw == 0) ?> <?= row("empty()?", empty($raw)) ?> <?= row("strlen", strlen($raw)) ?> <?= row("mb_strlen", mb_strlen($raw)) ?> </table> </body> </html> - Visit
http://192.168.0.19/type-inspector.php. - Try a series of inputs and look at the results carefully:
?v=42— notice (int) keeps it 42, (float) makes it 42, all comparisons sensible.?v=3.14— int cast truncates to 3, float keeps the decimal.?v=0— notice the (bool) cast is FALSE because "0" is falsy. The infamous trap.?v=(empty) — empty() returns true, both strlen counts are 0.?v=hello— int cast is 0 (no numeric prefix), bool is true.?v=héllo— notice strlen returns 6 (bytes!) but mb_strlen returns 5 (characters). The UTF-8 trap we mentioned earlier, in action.
Stretch goals:
- Add a row showing what
json_decode($raw)returns. Now you can paste JSON and see how PHP parses it. - Add a row showing
filter_var($raw, FILTER_VALIDATE_EMAIL)— useful for email validation experiments. - Add a "compare to" input so you can type two values and see all six combinations of
==/===between them.
What you flexed: All four scalar types, the truthy/falsy gotchas, === vs == behavior, type casting at the boundary, the strlen vs mb_strlen difference for UTF-8, function definitions with typed signatures (sneak preview of chapter 5), output escaping with htmlspecialchars. Genuinely useful debug tool from one little PHP file.