Core Syntax #
PHP is designed to be easy to learn — but “easy to learn” doesn’t mean there are no traps. Many developers new to PHP manage to write working code, yet without understanding a few important ground rules: when to use strict (===) vs loose (==) comparison, what the difference is between echo and print, or why PHP variables don’t need their types declared. This article covers all the fundamentals of PHP syntax thoroughly — not just “here’s the syntax”, but also “here’s why, and here’s what often goes wrong”. After reading it, you’ll have a solid enough understanding to write PHP code that’s correct, not just code that happens to run.
PHP Tags #
PHP is a language designed to be embedded in HTML. This works differently from JavaScript (which has <script> tags) — PHP uses its own special tags that tell the interpreter which parts need processing.
The standard PHP tag is <?php to open and ?> to close:
<?php
// All PHP code goes here
echo "Hello, World!";
?>
Tag Placement and Its Relationship with HTML #
PHP and HTML can coexist in the same file. The PHP interpreter only processes the parts inside the <?php ... ?> tags, while everything outside the tags is sent straight to the browser as HTML:
<!DOCTYPE html>
<html>
<body>
<h1>Welcome</h1>
<?php
$name = "Budi";
echo "<p>Hello, " . $name . "!</p>";
?>
<footer>Plain HTML footer</footer>
</body>
</html>
Omitting the Closing Tag #
When a PHP file contains only pure PHP code (no HTML at all), there’s an important convention that beginners often overlook:
<?php
// ANTI-PATTERN: closing the tag in a pure PHP file
class UserRepository
{
// ... class code
}
?>
<!-- The closing tag above can cause a "headers already sent" error
if there's any whitespace/newline after ?> -->
// CORRECT: omit the closing tag in a pure PHP file
class UserRepository
{
// ... class code
}
// No ?> at the end of the file — this is actually the right way
The closing ?> tag is optional at the end of a pure PHP file, and it’s better to leave it out. If any character (including an invisible space or newline) appears after ?>, PHP will send it as output before the HTTP headers, causing a confusing “headers already sent” error.
Short Echo Tag #
PHP also recognizes the short echo tag <?= as shorthand for <?php echo:
<!-- The long way -->
<?php echo $title; ?>
<!-- Short echo tag — more concise for HTML templates -->
<?= $title ?>
<!-- The short echo tag also supports expressions -->
<?= strtoupper($name) ?>
<?= count($items) . " items" ?>
The short echo tag <?= has been enabled by default since PHP 5.4 and is safe to use. It’s idiomatic for template files (the view layer) because it’s cleaner than <?php echo.
Variables #
PHP is a dynamically typed language — a variable’s type is determined at runtime based on the value assigned, not at declaration. Every PHP variable starts with a $ sign.
<?php
$name = "Budi"; // string
$age = 25; // integer
$height = 175.5; // float
$active = true; // boolean
$address = null; // null (no value yet)
Variable Naming Rules #
<?php
// ✓ Valid variable names
$name = "Budi";
$fullName = "Budi Santoso"; // camelCase
$full_name = "Budi Santoso"; // snake_case
$_config = []; // may start with an underscore
$score2 = 100; // may contain digits
// ✗ Invalid variable names
// $2score = 100; // must not start with a digit
// $name-me = "x"; // hyphens are not allowed
// $name me = "x"; // spaces are not allowed
PHP doesn’t enforce a naming convention, but the PHP community (including PSR-12) generally uses camelCase for variables and function parameters, and snake_case for database table and column names.
Variable Variables #
PHP has a unique feature called variable variables — variables whose value is used as the name of another variable:
<?php
$key = "name";
$$key = "Budi";
echo $name; // Output: Budi
echo $$key; // Output: Budi (same)
This feature is rarely used and best avoided because it makes code hard to read and debug. It’s mentioned here just so you don’t get confused when you come across it in someone else’s code.
Data Types #
PHP supports eight primitive data types. Understanding the differences between them — especially how PHP performs automatic conversion — is crucial for avoiding hard-to-track-down bugs.
Scalar Data Types #
| Type | Example Values | Description |
|---|---|---|
string | "Hello", 'World' | Text, can use single or double quotes |
int | 42, -10, 0 | Whole numbers |
float | 3.14, 1.5e3 | Decimal numbers |
bool | true, false | Boolean (not case-sensitive) |
<?php
$string = "PHP 8.3";
$integer = 42;
$float = 3.14159;
$boolean = true;
// Check the type with gettype() or var_dump()
var_dump($string); // string(6) "PHP 8.3"
var_dump($integer); // int(42)
var_dump($float); // float(3.14159)
var_dump($boolean); // bool(true)
Strings: Single Quotes vs Double Quotes #
This is one of the most important differences that’s often overlooked:
<?php
$name = "Budi";
// Double quotes: variables are interpolated (replaced with their values)
echo "Hello, $name!"; // Output: Hello, Budi!
echo "Hello, {$name}!"; // Output: Hello, Budi! (more explicit)
// Single quotes: text is shown as-is, NO interpolation
echo 'Hello, $name!'; // Output: Hello, $name!
echo 'Hello, {$name}!'; // Output: Hello, {$name}!
// Escape sequences only work in double quotes
echo "First line\nSecond line"; // newline works
echo 'First line\nSecond line'; // \n is shown literally
Use single quotes when the string doesn’t need interpolation — slightly faster because PHP doesn’t have to check for variables. Use double quotes when you need interpolation or escape sequences like \n and \t.
Compound Data Types #
<?php
// Array — a collection of values
$fruits = ["apple", "mango", "orange"];
$profile = ["name" => "Budi", "age" => 25];
// Object — an instance of a class
class Point {
public function __construct(
public float $x,
public float $y
) {}
}
$point = new Point(3.0, 4.0);
// Callable — something that can be called as a function
$double = fn($n) => $n * 2;
echo $double(5); // Output: 10
Null and Checking for It #
<?php
$data = null;
// Three ways to check for null — each behaves differently
var_dump($data === null); // bool(true) — strict comparison
var_dump(is_null($data)); // bool(true) — built-in function
var_dump(isset($data)); // bool(false) — checks whether set AND not null
Type Juggling — Automatic Conversion #
PHP automatically converts data types when needed. This is a convenience feature, but also a frequent source of bugs:
<?php
// PHP converts a string to an integer during arithmetic operations
$result = "10" + 5;
var_dump($result); // int(15) — string "10" converted to int
// Strings that don't start with a digit convert to 0
$result2 = "abc" + 5;
var_dump($result2); // int(5)
// Booleans in arithmetic contexts
$result3 = true + true;
var_dump($result3); // int(2)
This behavior is why strict comparison (===) is almost always safer than loose comparison (==) — covered in more detail in the Operators section.
Operators #
PHP has a rich set of operators. The comparison operators are the most important to understand properly, because their unintuitive behavior is a frequent source of bugs.
Arithmetic Operators #
<?php
$a = 17;
$b = 5;
echo $a + $b; // 22 — addition
echo $a - $b; // 12 — subtraction
echo $a * $b; // 85 — multiplication
echo $a / $b; // 3.4 — division (always float unless evenly divisible)
echo $a % $b; // 2 — modulo (remainder)
echo $a ** $b; // 1419857 — exponentiation (PHP 5.6+)
// Increment / Decrement
$c = 10;
echo $c++; // 10 — display first, then increment
echo $c; // 11
echo ++$c; // 12 — increment first, then display
Comparison Operators: Strict vs Loose #
This is the part most crucial to understand correctly:
<?php
// Loose operator (==) — compares values after type conversion
var_dump(0 == "foo"); // bool(true) in PHP 7, bool(false) in PHP 8!
var_dump(0 == ""); // bool(false) in PHP 8
var_dump(1 == "1"); // bool(true) — "1" converted to int 1
var_dump(null == false); // bool(true)
var_dump("" == false); // bool(true)
var_dump("0" == false); // bool(true)
var_dump(100 == "1e2"); // bool(true) — scientific notation!
// Strict operator (===) — compares value AND type
var_dump(1 === "1"); // bool(false) — different types
var_dump(1 === 1); // bool(true)
var_dump(null === false);// bool(false) — different types
The behavior of==changed between PHP 7 and PHP 8. In PHP 7,0 == "foo"evaluates totrue(non-numeric strings convert to 0). In PHP 8, the result isfalse. This is a breaking change that causes bugs during version migration. Use===to avoid this ambiguity entirely.
<?php
// ANTI-PATTERN: using == for conditions involving mixed types
function findUser($id) {
// Assume this function returns false if not found, or an array if found
// ...
}
if (findUser(0) == false) {
// BUG: if findUser() returns an empty array [],
// [] == false is true, but findUser() succeeded!
}
// CORRECT: use === for explicit comparison
if (findUser(0) === false) {
echo "User not found";
}
Operator Comparison Table #
| Operator | Name | Example | Result |
|---|---|---|---|
== | Equal (loose) | 1 == "1" | true |
=== | Identical (strict) | 1 === "1" | false |
!= | Not equal (loose) | 1 != "2" | true |
!== | Not identical (strict) | 1 !== "1" | true |
< | Less than | 1 < 2 | true |
> | Greater than | 2 > 1 | true |
<= | Less than or equal | 1 <= 1 | true |
>= | Greater than or equal | 2 >= 1 | true |
<=> | Spaceship | 1 <=> 2 | -1 |
The Spaceship Operator #
The <=> (spaceship) operator is very useful for sorting. It returns -1, 0, or 1:
<?php
// Very useful as a usort() callback
$numbers = [3, 1, 4, 1, 5, 9, 2, 6];
usort($numbers, fn($a, $b) => $a <=> $b); // ascending
// Output: [1, 1, 2, 3, 4, 5, 6, 9]
$products = [
["name" => "Laptop", "price" => 15000000],
["name" => "Mouse", "price" => 250000],
["name" => "Monitor","price" => 5000000],
];
usort($products, fn($a, $b) => $a["price"] <=> $b["price"]);
// Products sorted from lowest to highest price
Logical Operators #
<?php
$a = true;
$b = false;
var_dump($a && $b); // bool(false) — AND
var_dump($a || $b); // bool(true) — OR
var_dump(!$a); // bool(false) — NOT
var_dump($a xor $b); // bool(true) — XOR (one or the other, but not both)
// and / or have lower precedence than && / ||
// This can cause subtle bugs:
$x = true and false; // $x = true! because: ($x = true) and false
$y = true && false; // $y = false — this is what was intended
Null Coalescing Operator #
The ?? (null coalescing) operator is very useful for handling values that might be null:
<?php
// ANTI-PATTERN: verbose and repetitive
$name = isset($_GET['name']) ? $_GET['name'] : 'Guest';
// CORRECT: null coalescing operator (??)
$name = $_GET['name'] ?? 'Guest';
// Can be chained
$city = $_GET['city'] ?? $profile['city'] ?? 'Jakarta';
// Null coalescing assignment (??=) — PHP 7.4+
$config['timeout'] ??= 30; // set to 30 only if null or missing
Control Structures #
PHP supports all the control structures familiar from other languages, with a few handy extras.
If / Elseif / Else #
<?php
$score = 75;
if ($score >= 90) {
echo "A";
} elseif ($score >= 80) {
echo "B";
} elseif ($score >= 70) {
echo "C";
} elseif ($score >= 60) {
echo "D";
} else {
echo "E";
}
For simple conditions, use the ternary operator or a match expression (PHP 8.0+):
<?php
$age = 20;
// Ternary — for two-branch conditions
$status = $age >= 18 ? "adult" : "minor";
// Match expression — safer than switch (strict comparison, returns a value)
$day = 3;
$dayName = match($day) {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6, 7 => "Weekend",
default => "Invalid",
};
echo $dayName; // Output: Wednesday
Switch vs Match #
<?php
$color = "red";
// Switch: uses == (loose), can fall through if you forget break
switch ($color) {
case "red":
echo "Stop";
break; // don't forget the break!
case "yellow":
echo "Caution";
break;
default:
echo "Unknown";
}
// Match: uses === (strict), no fall-through, must be exhaustive
// If no arm matches and there's no default, it throws UnhandledMatchError
$message = match($color) {
"red" => "Stop",
"yellow" => "Caution",
"green" => "Go",
default => "Unknown",
};
echo $message;
Use match on PHP 8+ unless you deliberately need fall-through behavior.
Loops #
PHP provides four kinds of loops. Each has its right use case:
<?php
// while — when the condition is checked before each iteration
$i = 1;
while ($i <= 5) {
echo $i . " ";
$i++;
}
// Output: 1 2 3 4 5
// do-while — the body runs at least once
$input = "";
do {
// Simulate: keep asking for input until it's not empty
$input = "valid data"; // pretend this is user input
} while ($input === "");
// for — when the number of iterations is known in advance
for ($i = 0; $i < 5; $i++) {
echo $i . " ";
}
// Output: 0 1 2 3 4
// foreach — for iterating over arrays or iterables
$fruits = ["apple", "mango", "orange"];
foreach ($fruits as $index => $name) {
echo "$index: $name\n";
}
// Output:
// 0: apple
// 1: mango
// 2: orange
Break and Continue #
<?php
// break — exit the loop
for ($i = 0; $i < 10; $i++) {
if ($i === 5) break;
echo $i . " ";
}
// Output: 0 1 2 3 4
// continue — skip this iteration, move on to the next
for ($i = 0; $i < 10; $i++) {
if ($i % 2 === 0) continue; // skip even numbers
echo $i . " ";
}
// Output: 1 3 5 7 9
// break with an argument — exit N levels of nested loops
for ($i = 0; $i < 3; $i++) {
for ($j = 0; $j < 3; $j++) {
if ($j === 1) break 2; // exit both loops at once
echo "$i,$j ";
}
}
// Output: 0,0
Functions #
Functions in PHP have been first-class citizens since PHP 5.3 introduced closures. There are several ways to define functions, each with a different use context.
Regular Functions #
<?php
// Basic definition
function greet(string $name, string $greeting = "Hello"): string {
return "$greeting, $name!";
}
echo greet("Budi"); // Hello, Budi!
echo greet("Siti", "Good morning"); // Good morning, Siti!
Notice the type hints (string) for parameters and the return type. PHP 7+ supports type hints for all scalar types, and this is highly recommended because it makes code easier to understand and bugs surface faster.
Variadic Functions #
<?php
// A function that accepts an unlimited number of arguments
function sum(int ...$numbers): int {
return array_sum($numbers);
}
echo sum(1, 2, 3); // 6
echo sum(10, 20, 30, 40); // 100
// Spread operator — the opposite of variadic
$data = [1, 2, 3, 4, 5];
echo sum(...$data); // 15
Closures and Arrow Functions #
<?php
// Closure — an anonymous function
$multiply = function(int $a, int $b): int {
return $a * $b;
};
echo $multiply(3, 4); // 12
// A closure needs 'use' to access outer variables
$factor = 3;
$multiplyByThree = function(int $n) use ($factor): int {
return $n * $factor;
};
echo $multiplyByThree(5); // 15
// Arrow function (PHP 7.4+) — automatically captures outer variables
$multiplyByThreeArrow = fn(int $n): int => $n * $factor;
echo $multiplyByThreeArrow(5); // 15 (same, but more concise)
// Arrow functions are very useful as callbacks
$numbers = [1, 2, 3, 4, 5, 6];
$even = array_filter($numbers, fn($n) => $n % 2 === 0);
$double = array_map(fn($n) => $n * 2, $numbers);
Arrays #
Arrays in PHP are extremely flexible data structures — they can act as lists, dictionaries, stacks, queues, or any combination.
Indexed and Associative Arrays #
<?php
// Indexed (numeric) array
$fruits = ["apple", "mango", "orange"];
echo $fruits[0]; // apple
// Associative array (key-value)
$profile = [
"name" => "Budi Santoso",
"age" => 25,
"city" => "Jakarta",
];
echo $profile["name"]; // Budi Santoso
// Arrays can be nested
$data = [
"users" => [
["id" => 1, "name" => "Budi"],
["id" => 2, "name" => "Siti"],
],
"total" => 2,
];
echo $data["users"][0]["name"]; // Budi
The Most Commonly Used Array Functions #
<?php
$numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3];
// Array size
echo count($numbers); // 10
// Sorting
sort($numbers); // sort ascending, modifies the original array
$sorted = sorted($numbers); // doesn't exist, use:
$copy = $numbers;
sort($copy); // ✓ sort a copy, not the original
// Find elements
echo in_array(9, $numbers); // true
echo array_search(9, $numbers); // its index
// Transformations
$double = array_map(fn($n) => $n * 2, $numbers);
$large = array_filter($numbers, fn($n) => $n > 4);
$total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
// Slicing and splicing
$slice = array_slice($numbers, 0, 3); // take the first 3 elements
array_push($numbers, 99); // append at the end
$first = array_shift($numbers); // take and remove the first element
// Unique and merge
$unique = array_unique($numbers);
$merged = array_merge([1, 2], [3, 4]);
Superglobals #
Superglobals are PHP’s built-in variables available in every scope — inside functions, classes, and even included files. They’re the doorway through which data from the outside world enters a PHP application.
flowchart LR
A[Browser / Client] -- "Query String ?id=1" --> B["$_GET"]
A -- "Form POST" --> C["$_POST"]
A -- "Cookie" --> D["$_COOKIE"]
A -- "File upload" --> E["$_FILES"]
F[Server] -- "Server info" --> G["$_SERVER"]
H[Session] -- "Session data" --> I["$_SESSION"]
B & C & D & E & G & I --> J[PHP Application]List of Superglobals and What They Do #
| Superglobal | Contents | When to Use |
|---|---|---|
$_GET | Data from the URL query string | Receiving search parameters, filters, pagination |
$_POST | Data from forms using the POST method | Login, form submission, uploading data |
$_REQUEST | Combined GET + POST + COOKIE | Avoid — ambiguous and insecure |
$_FILES | Uploaded file data | Forms with enctype="multipart/form-data" |
$_SESSION | User session data | Login state, shopping cart |
$_COOKIE | Cookies from the browser | Remember me, user preferences |
$_SERVER | Server and request info | IP address, URL, method, headers |
$_ENV | Environment variables | Configuration from .env files |
$GLOBALS | All global variables | Avoid — makes code hard to debug |
Using Superglobals Safely #
Data from the superglobals$_GET,$_POST,$_COOKIE, and$_FILEScomes directly from users — it can’t be trusted. Always validate and sanitize before using it, especially before putting it into a database or displaying it on a page.
<?php
// ANTI-PATTERN: using user data directly without validation
$name = $_GET['name'];
echo "Hello, " . $name; // Vulnerable to XSS!
$id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $id"; // Vulnerable to SQL Injection!
// CORRECT: validate and sanitize first
$name = filter_input(INPUT_GET, 'name', FILTER_SANITIZE_SPECIAL_CHARS);
if ($name === null || $name === false) {
$name = "Guest";
}
echo "Hello, " . htmlspecialchars($name, ENT_QUOTES, 'UTF-8');
// For database queries, use prepared statements
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
// invalid id
http_response_code(400);
exit("Invalid ID");
}
// Continue with a PDO prepared statement
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]);
OOP — The Basics #
PHP has had full OOP support since version 5. Understanding the basics of OOP is essential because almost every modern PHP framework (Laravel, Symfony, CodeIgniter) is built on top of it.
Classes, Properties, and Methods #
<?php
class Product
{
// Properties — data owned by the object
public string $name;
public float $price;
private int $stock;
// Constructor — runs when the object is created
public function __construct(string $name, float $price, int $stock = 0)
{
$this->name = $name;
$this->price = $price;
$this->stock = $stock;
}
// Method — an action the object can perform
public function displayInfo(): string
{
return "{$this->name} — Rp " . number_format($this->price, 0, ',', '.');
}
// Getter for the private property
public function getStock(): int
{
return $this->stock;
}
// Method that modifies state
public function addStock(int $amount): void
{
if ($amount <= 0) {
throw new \InvalidArgumentException("Amount must be positive");
}
$this->stock += $amount;
}
}
// Creating an instance (object)
$laptop = new Product("ProBook Laptop", 15_000_000, 10);
echo $laptop->displayInfo(); // ProBook Laptop — Rp 15.000.000
echo $laptop->getStock(); // 10
$laptop->addStock(5);
echo $laptop->getStock(); // 15
Constructor Promotion (PHP 8.0+) #
PHP 8 introduced constructor promotion, which shortens property declarations:
<?php
// ANTI-PATTERN: the old way — properties declared twice
class LegacyUser
{
public string $name;
public string $email;
private int $age;
public function __construct(string $name, string $email, int $age)
{
$this->name = $name;
$this->email = $email;
$this->age = $age;
}
}
// CORRECT: constructor promotion — more concise, identical result
class User
{
public function __construct(
public string $name,
public string $email,
private int $age,
) {}
public function getAge(): int
{
return $this->age;
}
}
$user = new User("Budi", "[email protected]", 25);
echo $user->name; // Budi
echo $user->getAge(); // 25
Summary #
- Omit the closing
?>tag in pure PHP files — anything after it can cause a confusing “headers already sent” error.- Use
===instead of==for comparisons —==performs automatic type conversion whose results are often unpredictable, especially between PHP 7 and PHP 8.- Single vs double quotes — single quotes for literal strings, double quotes when you need variable interpolation or escape sequences.
- The null coalescing operator
??is the idiomatic way to handle values that might be null or undefined — far cleaner thanisset()+ ternary.matchis safer thanswitch— it uses strict comparison, has no fall-through, and throws an error if no arm matches.- Arrow functions
fn()automatically capture variables from the outer scope withoutuse— ideal for short callbacks.- Superglobals can’t be trusted — always validate and sanitize data from
$_GET,$_POST, and$_COOKIEbefore using it.- Constructor promotion (PHP 8+) eliminates property declaration boilerplate — take advantage of it for cleaner classes.