Beginner

Variables and Types in PHP

Learn how PHP handles variables, data types, type juggling, casting, and modern type declarations with Docker-ready examples

PHP is a dynamically typed language with weak typing: variables do not require a type declaration, and the interpreter happily converts between types when operations demand it. A single variable can hold an integer one moment and a string the next.

That flexibility is part of why PHP became the dominant language of the early web — it simply got out of your way. But the trade-off is that type-related bugs can be subtle. Modern PHP (7.0+) addresses this with optional scalar type hints, return types, union types, and declare(strict_types=1) to opt into stricter behavior. This tutorial covers both the traditional dynamic style and the modern typed style you’ll see in professional PHP codebases today.

By the end, you will know how to declare variables with the $ prefix, recognize PHP’s core scalar types, inspect types at runtime, cast between them deliberately, understand the type juggling that happens automatically, and define constants and type-hinted classes.

Variables, Scalar Types, and Type Juggling

Every PHP variable is prefixed with a dollar sign ($). PHP recognizes four scalar types — int, float, string, bool — plus null, array, and object. The type is determined by what you assign, not by any declaration.

Create a file named variables.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?php
// Dynamic variables - no type declaration needed
$age      = 30;        // int
$price    = 19.99;     // float (internally "double")
$name     = "Alice";   // string
$isActive = true;      // bool
$nothing  = null;      // null

// Double-quoted strings interpolate $variables directly
echo "Name: $name\n";
echo "Age: $age\n";
echo "Price: \$$price\n";
echo "Active: " . ($isActive ? 'yes' : 'no') . "\n";
echo "Nothing: " . var_export($nothing, true) . "\n";

// Type inspection
echo "\n--- Types ---\n";
echo "age is "      . gettype($age)      . "\n";
echo "price is "    . gettype($price)    . "\n";
echo "name is "     . gettype($name)     . "\n";
echo "isActive is " . gettype($isActive) . "\n";
echo "nothing is "  . gettype($nothing)  . "\n";

// Explicit type casting
echo "\n--- Type Conversion ---\n";
$asInt    = (int)    "42";
$asFloat  = (float)  "3.14";
$asString = (string) 100;
$asBool   = (bool)   0;   // 0, "", "0", null, and [] are all falsy
echo "(int) '42' = $asInt\n";
echo "(float) '3.14' = $asFloat\n";
echo "(string) 100 = '$asString'\n";
echo "(bool) 0 = " . var_export($asBool, true) . "\n";

// Type juggling: PHP's weak typing in action
echo "\n--- Type Juggling ---\n";
$sum    = "5" + 3;   // arithmetic promotes "5" -> int, result is 8 (int)
$concat = "5" . 3;   // concatenation promotes 3  -> string, result is "53"
echo "'5' + 3 = $sum (" . gettype($sum) . ")\n";
echo "'5' . 3 = '$concat' (" . gettype($concat) . ")\n";

// Loose vs strict comparison
echo "\n--- Comparison ---\n";
var_dump(1 == "1");    // loose:  values compared after juggling
var_dump(1 === "1");   // strict: types must also match

// Constants: fixed values that cannot be reassigned
const MAX_USERS = 100;
define('SITE_NAME', 'CodeArchaeology');
echo "\n--- Constants ---\n";
echo "MAX_USERS = " . MAX_USERS . "\n";
echo "SITE_NAME = " . SITE_NAME . "\n";

A few details worth calling out:

  • gettype($price) returns "double" — a historical quirk. PHP floats are IEEE-754 doubles, and the name stuck even though the language keyword is float.
  • Dollar sign in a double-quoted string must be escaped as \$, otherwise PHP tries to interpolate it as a variable name.
  • const vs define()const is evaluated at compile time and works inside classes; define() is a runtime function and can accept a computed name. Both produce constants that cannot be reassigned.

Modern Type Declarations

Since PHP 7, function parameters, return values, and class properties can be annotated with types. Adding declare(strict_types=1) at the top of a file disables the implicit conversions you saw above — passing a string where an int is expected becomes a TypeError instead of a silent coercion.

Create a file named typed_variables.php:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
declare(strict_types=1);

// Scalar type hints and return type
function greet(string $name, int $age): string {
    return "Hello, $name! You are $age years old.";
}

// Nullable type: ?int means int or null
function findUser(?int $id): ?string {
    if ($id === null) {
        return null;
    }
    return "User #$id";
}

// Union types (PHP 8+): accepts int or string
function formatId(int|string $id): string {
    return "ID: $id";
}

echo greet("Alice", 30) . "\n";
echo greet("Bob", 25) . "\n";

// Null coalescing operator ?? returns the right side if left is null
echo (findUser(42)   ?? "No user") . "\n";
echo (findUser(null) ?? "No user") . "\n";

echo formatId(7)         . "\n";
echo formatId("abc-123") . "\n";

// Typed properties (PHP 7.4+), readonly (PHP 8.1+),
// and constructor property promotion (PHP 8.0+)
class Product {
    public function __construct(
        public readonly string $name,
        public readonly float  $price,
        public int $stock = 0,
    ) {}
}

$item = new Product(name: "Book", price: 12.99, stock: 5);
echo "{$item->name}: \${$item->price} (stock: {$item->stock})\n";

Under strict_types=1, calling greet("Alice", "30") would throw a TypeError because "30" is not an int — even though it could be converted to one. Without the declaration, PHP would coerce silently. Most modern PHP projects enable strict types in every file.

Running with Docker

1
2
3
4
5
6
7
8
# Pull the official PHP CLI image
docker pull php:8.4-cli-alpine

# Run the dynamic-typing example
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php variables.php

# Run the typed example
docker run --rm -v $(pwd):/app -w /app php:8.4-cli-alpine php typed_variables.php

Expected Output

Output of variables.php:

Name: Alice
Age: 30
Price: $19.99
Active: yes
Nothing: NULL

--- Types ---
age is integer
price is double
name is string
isActive is boolean
nothing is NULL

--- Type Conversion ---
(int) '42' = 42
(float) '3.14' = 3.14
(string) 100 = '100'
(bool) 0 = false

--- Type Juggling ---
'5' + 3 = 8 (integer)
'5' . 3 = '53' (string)

--- Comparison ---
bool(true)
bool(false)

--- Constants ---
MAX_USERS = 100
SITE_NAME = CodeArchaeology

Output of typed_variables.php:

Hello, Alice! You are 30 years old.
Hello, Bob! You are 25 years old.
User #42
No user
ID: 7
ID: abc-123
Book: $12.99 (stock: 5)

Key Concepts

  • Sigil-prefixed variables — every variable is written with a leading $, and names are case-sensitive. Function and class names are case-insensitive; variables are not.
  • Dynamic and weak by default — assigning to a variable determines its type, and arithmetic or concatenation will coerce operands silently. "5" + 3 is 8; "5" . 3 is "53".
  • Seven primary typesint, float, string, bool, array, object, and null. gettype() returns "double" for floats and "NULL" for null, for historical reasons.
  • Explicit casting uses parenthesized type names: (int), (float), (string), (bool), (array). Prefer explicit casts over relying on juggling in real code.
  • Strict vs loose equality== applies type juggling before comparing; === requires identical types as well as values. Default to === to avoid surprises.
  • Type declarations are optional but encouraged — PHP 7+ supports scalar type hints, return types, nullable types (?int), and union types (int|string). Add declare(strict_types=1) to turn silent coercions into TypeErrors.
  • const and define() both create constants, but const is compile-time and scope-aware, while define() is a runtime function. Constants are written without the $ sigil when referenced.
  • Readonly and typed properties (PHP 7.4 / 8.1) bring immutability and type safety to classes, making modern PHP feel closer to statically typed languages without giving up its scripting roots.

Running Today

All examples can be run using Docker:

docker pull php:8.4-cli-alpine
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining