PHP $_GET and $_POST
Whenever a browser sends information to a PHP script — by clicking a link with a query string, or by submitting an HTML form — that data has to land somewhere PHP code can read it. $_GET and $_POST are the two superglobal arrays that hold exactly that data: the parameters attached to a URL, and the fields submitted in a form’s request body. Nearly every dynamic PHP feature, from a search box to a login form to a shopping cart, starts with reading one of these arrays. This lesson covers how they work internally, how to use them correctly, and the security mistakes that trip up almost every beginner.
Overview: How $_GET and $_POST Work
HTTP is the protocol browsers and servers use to talk to each other. Every request has a method (most commonly GET or POST), a URL, a set of headers, and optionally a body. $_GET and $_POST are PHP’s way of exposing the data attached to that request as ordinary associative arrays, so you never have to parse raw HTTP text yourself.
$_GET holds the query string — the part of a URL after the ?, made of key=value pairs joined with &. For example, visiting products.php?category=books&sort=price gives you $_GET['category'] equal to "books" and $_GET['sort'] equal to "price". Crucially, $_GET is populated from the URL regardless of the HTTP method — even a POST request can carry query-string parameters that end up in $_GET.
$_POST holds data from the request body, and is only populated when the request method is POST and the Content-Type header is application/x-www-form-urlencoded or multipart/form-data — the two encodings an HTML <form> uses by default. If a client sends a POST request with a JSON body (Content-Type: application/json), $_POST stays empty; you’d read the raw body with file_get_contents('php://input') instead and decode it yourself.
Both arrays are superglobals: PHP populates them before any of your code runs, and they’re automatically available inside every function and class method without needing a global statement or being passed as a parameter. Internally, the SAPI layer (e.g. PHP-FPM, the Apache module, or the CLI server) hands the raw request to the Zend Engine’s request-startup routine, which parses the query string and, for eligible POST bodies, the body too, URL-decoding each key and value along the way and building the arrays your script sees. This happens once, at the very start of the request; values are always strings (or nested arrays of strings for bracket-style field names like tags[]), never integers, booleans, or objects — even a field that looks numeric, like an age of 17, arrives as the string "17".
Syntax
$value = $_GET['key'] ?? $default;
$value = $_POST['key'] ?? $default;
$value = $_REQUEST['key'] ?? $default;
$value = filter_input(INPUT_GET, 'key', FILTER_VALIDATE_INT);
| Part | Meaning |
|---|---|
$_GET['key'] |
Reads the URL query-string parameter named key. Throws a warning if it doesn’t exist. |
$_POST['key'] |
Reads the form field named key from a POST request body. |
$_REQUEST['key'] |
A merged array of $_GET, $_POST, and $_COOKIE (order controlled by the request_order ini setting). Generally best avoided since you can’t tell where a value came from. |
?? |
The null coalescing operator. Returns the left side if it’s set and not null, otherwise the right side — the standard way to avoid “undefined array key” warnings. |
filter_input() |
Reads and validates a single input value straight from the original request (INPUT_GET or INPUT_POST) using a filter constant like FILTER_VALIDATE_INT or FILTER_VALIDATE_EMAIL, returning false on failure. |
Examples
Example 1: Reading a Search Query from $_GET
<?php
// Simulate a URL like: search.php?query=laptop&category=electronics
$_GET['query'] = 'laptop';
$_GET['category'] = 'electronics';
$query = isset($_GET['query']) ? trim($_GET['query']) : '';
$category = $_GET['category'] ?? 'all';
echo "Searching for '{$query}' in category '{$category}'." . PHP_EOL;
if ($query === '') {
echo "Please enter a search term." . PHP_EOL;
} else {
echo "Showing results for: " . htmlspecialchars($query) . PHP_EOL;
}
Output:
Searching for 'laptop' in category 'electronics'.
Showing results for: laptop
This mirrors a typical search page. The parameters come straight from the URL, so the page is bookmarkable and shareable — a defining property of GET requests. Notice the defensive use of isset() and ??: if a visitor loads search.php with no query string at all, the script still runs cleanly instead of throwing warnings.
Example 2: Validating a POST Form Submission
<?php
// Simulate a form submission from a POST request
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['username'] = 'ana_dev';
$_POST['age'] = '17';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$age = filter_var($_POST['age'] ?? '', FILTER_VALIDATE_INT);
$errors = [];
if ($username === '') {
$errors[] = 'Username is required.';
}
if ($age === false || $age < 18) {
$errors[] = 'You must be at least 18 years old.';
}
if (empty($errors)) {
echo "Welcome, {$username}!" . PHP_EOL;
} else {
foreach ($errors as $error) {
echo "Error: {$error}" . PHP_EOL;
}
}
}
Output:
Error: You must be at least 18 years old.
Every value pulled from $_POST is a string, so '17' has to be converted with filter_var() before it can be compared numerically. The script checks $_SERVER['REQUEST_METHOD'] first, a pattern you'll see constantly: the same PHP file often renders a form and processes its submission, branching on whether the request was a GET (show the empty form) or a POST (validate and act on the data).
Example 3: Combining $_GET and $_POST
<?php
// Simulate: edit.php?id=42 submitted via POST
$_GET['id'] = '42';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['email'] = 'ana@example.com';
$_POST['newsletter'] = 'yes';
$id = filter_var($_GET['id'] ?? null, FILTER_VALIDATE_INT);
if ($id === false || $id === null) {
echo "Invalid or missing ID." . PHP_EOL;
exit;
}
echo "Updating profile #{$id}" . PHP_EOL;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
$subscribed = ($_POST['newsletter'] ?? 'no') === 'yes';
if ($email === false) {
echo "Invalid email address." . PHP_EOL;
} else {
echo "New email: {$email}" . PHP_EOL;
echo "Newsletter: " . ($subscribed ? 'subscribed' : 'not subscribed') . PHP_EOL;
}
}
Output:
Updating profile #42
New email: ana@example.com
Newsletter: subscribed
It's completely normal for a single request to carry both kinds of data at once: the record ID travels in the URL (?id=42), identifying what to update, while the new values travel in the POST body, carrying the data to write. This keeps the URL meaningful and shareable while keeping the sensitive or bulky payload out of the address bar.
How It Works Step by Step (Under the Hood)
When a request arrives, PHP builds $_GET and $_POST before a single line of your script executes:
- The web server (Apache, Nginx, or the built-in CLI server) receives the raw HTTP request and hands it to the PHP SAPI (e.g. PHP-FPM or
mod_php). - During request startup, the Zend Engine's input-parsing routine (
php_default_treat_data) splits the URL's query string on&, then on=, URL-decodes each piece, and inserts it into$_GET. Repeated bracket names liketags[]=a&tags[]=bbecome a nested array,['a', 'b']. - If the method is
POSTand theContent-Typeis a form encoding, PHP reads the request body (respecting thepost_max_sizeini limit) and parses it the same way into$_POST; file uploads inside amultipart/form-databody are split out into$_FILESinstead. $_REQUESTis then assembled by merging$_GET,$_POST, and$_COOKIEin the order given by therequest_orderini directive — later sources overwrite earlier ones on key collision.- These arrays are ordinary PHP arrays sitting in memory for the lifetime of the request. Assigning to
$_GET['x'] = 'y'in your own code (as the examples above do to simulate incoming data) doesn't talk to the network at all — it just mutates the array like any other variable. - Once the script finishes, the arrays are discarded. Nothing persists to the next request unless you explicitly save it (to a session, a database, a cookie, and so on).
Common Mistakes
Mistake 1: Trusting input without validating or escaping it
Wrong:
<?php
// No validation and no escaping - risky
echo "<p>You searched for: " . $_GET['q'] . "</p>";
This has two problems. First, if a visitor loads the page without a q parameter, PHP raises an "Undefined array key" warning. Second, and far more serious: whatever the visitor puts in q is written straight into the HTML response with no escaping. A URL like ?q=<script>stealCookies()</script> would have its script tag executed in every other visitor's browser who views that output — a classic reflected Cross-Site Scripting (XSS) vulnerability.
Corrected:
<?php
// Simulate an incoming request with a malicious payload
$_GET['q'] = '<script>alert(1)</script>';
$search = htmlspecialchars($_GET['q'] ?? '', ENT_QUOTES, 'UTF-8');
echo "<p>You searched for: {$search}</p>" . PHP_EOL;
Output:
<p>You searched for: <script>alert(1)</script></p>
The ?? operator supplies a safe default when the key is missing, and htmlspecialchars() converts <, >, &, and quote characters into their HTML entity equivalents, so the browser displays the text instead of executing it. As a rule: never print a superglobal value into HTML without escaping it first.
Mistake 2: Using GET for actions that change data
Wrong:
<?php
// Deleting a record based on a GET request - dangerous!
$_GET['id'] = '17';
$_GET['action'] = 'delete';
if ($_GET['action'] === 'delete') {
echo "Deleting record #{$_GET['id']}..." . PHP_EOL;
}
The HTTP specification says GET requests must be safe and idempotent — they should only retrieve data, never change it. Browsers, crawlers, and link-prefetching tools all assume this and will happily trigger GET requests you never intended, such as a search bot following a "delete" link. Because there's no built-in protection against a third-party page simply embedding <img src="https://example.com/delete.php?id=17&action=delete">, this pattern is also a textbook Cross-Site Request Forgery (CSRF) hole.
Corrected:
<?php
$_SERVER['REQUEST_METHOD'] = 'POST';
$_POST['id'] = '17';
$_POST['action'] = 'delete';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete') {
$id = filter_var($_POST['id'] ?? null, FILTER_VALIDATE_INT);
if ($id !== false && $id !== null) {
echo "Deleting record #{$id}..." . PHP_EOL;
}
}
Output:
Deleting record #17...
Requiring a POST (submitted from a real <form>, ideally with a CSRF token) makes the action much harder to trigger accidentally or maliciously. As a habit: use GET only for requests that just fetch or filter data, and POST for anything that creates, updates, or deletes something.
Best Practices
- Always guard against missing keys with
??orisset()— never assume a parameter was sent. - Validate and convert types with
filter_var()orfilter_input()rather than trusting raw strings, especially for numbers, emails, and URLs. - Escape every value with
htmlspecialchars()before printing it into HTML to prevent XSS. - Use GET only for safe, idempotent operations (search, filtering, pagination); use POST for anything that changes server state.
- Never put passwords, tokens, or other sensitive data in a GET request — query strings end up in browser history, server access logs, and the
Refererheader of any link the page follows. - Avoid
$_REQUESTin new code; it hides whether a value came from the URL or a form body, which matters for both security and debugging. - Add a CSRF token to POST forms that perform sensitive actions, and verify it on submission.
- Remember all superglobal values are strings (or arrays of strings) — cast or validate before doing arithmetic or comparisons.
Practice Exercises
- Write a script that simulates
$_GET['page']and$_GET['limit']for a paginated product listing. Validate both as positive integers usingfilter_var(), defaulting topage = 1andlimit = 10if they're missing or invalid, then echo the resulting values. - Simulate a POST login form with
$_POST['email']and$_POST['password']. Validate that the email passesFILTER_VALIDATE_EMAILand that the password is at least 8 characters long, printing a specific error message for each failed rule. - Build a small "contact form" simulation that checks
$_SERVER['REQUEST_METHOD']: if it'sGET, echo"Please fill out the form."; if it'sPOST, read andhtmlspecialchars()-escape amessagefield and echo it back safely.
Summary
$_GETholds URL query-string parameters; it's populated regardless of the HTTP method used.$_POSTholds form-body data, and is only populated for POST requests with a form-encoded content type.- Both are superglobal arrays of strings, built by PHP before your script runs, and available in every scope without extra setup.
- Always check for missing keys with
??orisset(), and validate values withfilter_var()/filter_input(). - Escape output with
htmlspecialchars()to prevent XSS, and use POST (not GET) for any action that changes data, to avoid CSRF and accidental triggers. $_REQUESTmerges GET, POST, and cookies but is generally best avoided in favor of being explicit.
