πŸ“– 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 $_SESSION superglobal 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=value format. 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.php and looks up a user record by username using a prepared SQL statement. It returns an associative array if found, or false if 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 exit immediately 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 false if 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 ?logout to 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.

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.

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.

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.

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() and password_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