π PHP Sessions and Cookies
Preparing for Session-Based Access
As we begin to manage user login and profile access, we need a reliable way to control PHP sessions across protected pages. To do this, we'll use a shared initialization file.
Creating a Shared init.php File
Create a file in your project root named init.php. This file should be included at the top of any entry point that needs to start a session or define global settings like the app's timezone.
// config/init.php
session_start(); // Start or resume the session
date_default_timezone_set('America/Chicago'); // Set your local timezone
- session_start()
- Initializes a new session or resumes an existing one. This function must be called before any output is sent to the browser. It enables access to the
$_SESSIONsuperglobal array, which stores data tied to a specific user session.
Use this file at the top of pages that require login or session features, such as:
// login.php or profile.php
require_once 'config/init.php';
require 'controllers/UserController.php';
π‘ Tip: Starting the session early in each entry point keeps your session logic predictable and avoids common errors caused by starting sessions after output has begun.
Project Structure
Use the following file structure to organize your work:
βββ project-root/
βββ config/
β βββ init.php β *new* handles global settings like session start and timezone
βββ controllers/
β βββ UserController.php β handles form logic and insertion
βββ models/
β βββ db_connect.php β PDO database connection
β βββ UserModel.php β contains the INSERT and SELECT queries
βββ views/
β βββ login.php β *new* form UI for login
β βββ partials/
β β βββ header.php β shared HTML header and navigation
β β βββ footer.php β shared footer and closing tags
β βββ profile/
β βββ create.php β displays the registration form
β βββ deactivate.php β confirmation form for deactivation
β βββ edit.php β displays the edit profile form
β βββ show.php β displays the user profile view
β βββ partials/
β βββ form-fields.php β shared form fields for create and edit views
βββ deactivate.php β entry point for soft delete POST requests
βββ login.php β *new* handles login logic using UserController
βββ profile.php β entry point for profile view/edit/update
βββ register.php β entry point for user registration
What Is Persistence?
Persistence allows you to remember users or data between page loads. HTTP is stateless β once a page is delivered, the connection is closed. To maintain user state or preferences across requests, you must use one of the following techniques:
- Session
- Server-based memory that stores user data (e.g., ID, name) for the current browser session. Sessions are temporary and secure.
- Cookie
- Client-based data stored in the user's browser. Good for remembering preferences or greeting users between visits, but not secure enough for login validation.
- Query String
- Appends data to the end of a URL using a
?key=valueformat. Visible to users and insecure for sensitive data. - Hidden Form Field
- Hidden data stored in HTML forms. Useful for tracking state across form steps, but not secure on its own.
Building the Login Workflow
To add login functionality to your MVC site, you'll create a coordinated workflow that includes an entry point script, a controller function, a login form view, and a model query. Each part plays a specific role in validating credentials and managing persistence.
π‘ Tip: Watch out for duplicate filenames that serve different purposes. For example, login.php in the project root is an entry point (controller logic), while views/login.php is a view file (UI only). Though their names match, their responsibilities and contents are very different β be mindful when referencing or including them.
Entry Point: login.php
This file acts as a page-level entry point. It delegates processing to the controller function login_user(), which handles both form validation and output.
<?php
require_once 'config/init.php';
require 'controllers/UserController.php';
login_user(); // Handles form processing and shows the login view
π‘ Tip: Only add require_once 'config/init.php'; to pages that use or check $_SESSION variables β like login, logout, and protected areas. This keeps your session logic clear and efficient.
views/login.php
This view renders the login form and displays any error messages passed in from the controller. Like the registration view, it uses partials for layout and preserves sticky input for the username field.
<?php include 'views/partials/header.php';
$pageTitle = "Login"; ?>
<h2>Login</h2>
<?php if (!empty($errors['login'])): ?>
<p class="error"><?= htmlspecialchars($errors['login']) ?></p>
<?php endif; ?>
<form method="POST" action="login.php">
<label for="username">Username</label>
<input type="text" name="username" id="username" required
value="<?= htmlspecialchars($post['username']) ?>">
<label for="password">Password</label>
<input type="password" name="password" id="password" required>
<button type="submit">Login</button>
</form>
<?php include 'views/partials/footer.php'; ?>
UserController.php
The login_user() function processes form data submitted via POST. If valid, it authenticates the user, starts a session, and redirects. If invalid, it returns the user to the form with errors. It always loads the form view as a final step.
function login_user()
{
$post = ['username' => ''];
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$post['username'] = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
$user = findByUsername($post['username']);
if ($user && $password === $user['password']) {
$_SESSION['userID'] = $user['id']; // Stores the user's ID for access checks
$_SESSION['username'] = $user['username']; // Stores their username for use across pages
setcookie('username', $user['username'], time() + 60*60*24*30); // Optional: greets returning users
header("Location: profile.php?id=" . $user['id']); // Redirects to the protected user profile page
exit;
} else {
$errors['login'] = 'β Invalid username or password.';
}
}
require 'views/login.php';
}
- findByUsername()
- This function lives in
UserModel.phpand looks up a user record by username using a prepared SQL statement. It returns an associative array if found, orfalseif no match exists. The password field in this version is stored in plain text, so comparison is done using===. - $_SESSION['userID']
- Used to confirm a user is logged in on other pages β for example, to show or hide menu options, or restrict content.
- $_SESSION['username']
- Available globally during the session to personalize messages like "Welcome, John."
- setcookie()
- Stores a value on the user's device so you can greet them by name when they return β even if their session has expired. Not secure for login checks.
- header('Location: profile.php')
- Performs a server-side redirect after successful login. Always call
exitimmediately after to stop execution.
β οΈ Important: This example compares plain text passwords directly for now. In the next article, you'll learn how to securely hash and verify passwords using password_hash() and password_verify(), which should be used in all production applications.
UserModel.php
This file contains the database query to find a user by username. It uses prepared statements to prevent SQL injection and returns the user data if found.
function findByUsername($username)
{
$stmt = $pdo->prepare("SELECT id, username, password FROM users WHERE username = :username");
$stmt->execute(['username' => $username]);
return $stmt->fetch();
}
- $pdo->prepare()
- Prepares a SQL statement for execution. This is a secure way to handle user input in queries.
- $stmt->execute()
- Executes the prepared statement with the provided parameters, returning the result set.
- $stmt->fetch()
- Fetches the next row from the result set as an associative array. Returns
falseif no rows match.
Accessing the Profile Page
After a successful login, we use a session variable to track which user is logged in. Protected pages like profile.php should check for this session variable and use it to load user-specific content.
// profile.php
require_once 'config/init.php';
$userId = $_SESSION['userID'] ?? ($_GET['id'] ?? null);
if (!$userId) {
header('Location: login.php');
exit;
}
- $_SESSION['userID']
- Identifies the currently logged-in user. This value is set during login and used throughout the session for access control.
- $_GET['id']
- Fallback used by the registration process. The
register_user()function currently redirects using a query string. This logic allows both workflows to operate until registration is updated to use sessions too.
β οΈ Note: Eventually, all authenticated page access should rely on $_SESSION['userID'] to improve security and simplify routing. This fallback is temporary to support the current registration flow.
Logging Out via Controller
To maintain a clean architecture, the logout process is now handled by a function in the UserController and routed through profile.php. This ensures consistent access control and simplifies logic reuse.
Header Button Logic
Update views/partials/header.php to show a login or logout button based on the session:
<?php if (isset($_SESSION['userID'])): ?>
<a href="profile.php?logout" class="btn btn-success">Logout</a>
<?php else: ?>
<a href="login.php" class="btn btn-success">Login</a>
<?php endif; ?>
- $_SESSION['userID']
- Used here to determine whether a user is logged in and control which navigation option is shown.
- profile.php?logout
- The logout button links to the profile page with a query parameter that triggers the logout controller function.
- ?logout
- Appending
?logoutto the profile URL triggers the logout logic via a controller call. This maintains consistency with how other controller-based requests are handled.
Routing the Logout
To trigger logout from the interface, add logic to the top of profile.php:
if (isset($_GET['logout'])) {
logout_user();
}
Controller Function
Add the following to UserController.php:
function logout_user() {
session_unset();
session_destroy();
header('Location: login.php?msg=logged_out');
exit;
}
- logout_user()
- This controller function ends the session and redirects the user to the login page with an optional message.
How to Use Cookies
Cookies allow you to store small pieces of information on the user's computer that persist across visits. They're ideal for remembering non-sensitive preferences such as a user's name for a personalized greeting. Unlike sessions, cookies survive after the browser is closed and can be accessed by future visits to your site.
Setting a Cookie
Use the setcookie() function in your controller logic (before any HTML output) to store a value. For example, during login:
setcookie('username', $user['username'], time() + 60 * 60 * 24 * 30);
This sets a cookie named username that lasts for 30 days.
Accessing a Cookie
You can check for and read cookie values using the $_COOKIE superglobal. This is helpful for personalization β not security.
if (isset($_COOKIE['username'])) {
echo "Welcome back, {$_COOKIE['username']}!";
} else {
echo "Welcome, guest.";
}
- $_COOKIE[]
- PHP superglobal array that retrieves client-stored cookie values. Cookies are sent with every request and should only be used for display or preference purposes β never authentication.
Global Greeting with Session or Cookie
To greet users on any page, define a global display name using either the active session or fallback cookie. Place this in config/init.php so it loads for every request:
session_start();
date_default_timezone_set('America/Chicago');
$displayName = $_SESSION['username'] ?? ($_COOKIE['username'] ?? null);
We use a variable like $displayName to hold the current user identity, whether active or returning, and make it globally available.
Dynamic Content Based on Login State
This code checks if a user is logged in (session) or has a cookie set, and displays a personalized greeting accordingly. If neither is available, it defaults to "Welcome, guest."
Use the $displayName state to display name in views/partials/header.php to add a personalized greeting.
<?php if ($displayName): ?>
<p>Welcome, <?= htmlspecialchars($displayName) ?>!</p>
<?php else: ?>
<p>Welcome, guest.</p>
<?php endif; ?>
Along with showing a personalized greeting, you can also conditionally display navigation links like Register, Sign In, and Logout based on whether the user is authenticated. This improves user experience and helps guide users to the correct actions based on their state.
Header Button Logic
To implement this, add logic to views/partials/header.php to show either a login or logout button based on the session state.
<?php if (isset($_SESSION['userID'])): ?>
<a href="profile.php?logout=true" class="btn btn-success">Logout</a>
<?php else: ?>
<a href="register.php" class="btn btn-success">Register</a>
<a href="login.php" class="btn btn-success">Sign In</a>
<?php endif; ?>
Place this logic inside header.php so that every page reflects the user's current access status. It's also a practical way to reduce user error β for example, hiding the Register link if someone is already logged in.
π‘ Tip: Use session or cookie checks in your header to personalize the greeting and control which links users see. This improves user experience and serves as a lightweight access control strategy by removing options that shouldn't be followed β like hiding Register or Sign In for logged-in users.
Choosing Between Session, Cookie, or Default
This comparison table helps you choose the best data source based on security and availability. Use this logic to determine how to greet a visitor and decide what content to show. You want to prioritize secure, current session data first, fall back to existing cookies if available, and use a default if neither is present. This decision-making process can guide other parts of your site as well.
| Source | When to Use | Example Use | Pros | Cons |
|---|---|---|---|---|
$_SESSION |
User is logged in | Personalized greeting, access control | Secure, server-side, current | Lost when browser closes |
$_COOKIE |
User has visited before, but is not logged in | βWelcome backβ message | Long-term memory, simple to access | Client-side, not secure, can be stale |
| Default | No session or cookie present | βWelcome, guest.β message | Always available | Not personalized |
π‘ Tip: Think of cookies as leftovers β useful if you have no fresh data, but not something you want to rely on exclusively.
Page Considerations
Each publicly accessible script (entry point) in your app may need different logic based on authentication status. For example:
- register.php: Should restrict or redirect authenticated users away β they don't need to register again.
- profile.php: Should require login and session check before displaying protected content.
- index.php: Can use cookies for a friendly greeting, even if not logged in.
Think about the user's state and what they should (or shouldn't) see based on that state. Building that logic into your entry points makes your app more intuitive and secure.
Best Practices
- Use
session_start()before accessing any session data - Store login info in
$_SESSION[], not cookies - Use
password_hash()andpassword_verify()to secure credentials (next article) - Always validate and sanitize user input before querying the database
- Set cookies before any HTML output and only for non-sensitive data
Summary
- Sessions store user data securely on the server for short-term use
- Cookies help personalize the experience across visits
- Login logic belongs in controllers, and database access should be done with prepared statements
- Always clean up sessions at logout and avoid storing sensitive data in cookies
Last updated: August 8, 2025 at 4:20 PM