Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Add the item to the cart before asking a visitor to log in. Keep a guest cart in the PHP session, protect checkout with a server-side authentication check, then regenerate the session ID and merge the guest cart into the customer’s database cart after successful login. Reload the cart and recalculate prices and stock from the database before checkout; a PHP session alone does not make those values trustworthy.

The request flow

  1. The visitor submits a product ID and quantity to a POST add-to-cart endpoint.
  2. The server validates them and stores the item in a guest session cart, or updates the signed-in customer’s database cart.
  3. When the visitor requests checkout, the server checks authentication. If needed, it saves a safe internal return destination and redirects to login.
  4. After verifying the password, the server regenerates the session ID, marks the customer as authenticated, and merges the guest cart into the customer’s cart.
  5. The browser returns to checkout, which reloads the cart and validates current products, prices, quantities, and stock.

PHP sessions can carry values between requests through $_SESSION, but only when the session is started and the browser continues to send the session cookie. See PHP’s session_start() documentation and its page on $_SESSION.

1. Add items to a guest cart

Start the session before output or any access to session data. Configure cookie options first, if you set them. For an HTTPS site, a typical starting point is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
<?php
declare(strict_types=1);

session_set_cookie_params([
    'lifetime' => 0,
    'path' => '/',
    'secure' => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

Use HTTPS with the secure setting. SameSite policy should suit your site and any cross-site payment flow; do not assume one setting fits every provider. Cookie parameters must be set before session_start().

A minimal POST handler can store product IDs and quantities in the session for guests. Signed-in customers should instead update their database cart:

<?php
// cart-add.php

declare(strict_types=1);
session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method not allowed.');
}

$productId = filter_input(INPUT_POST, 'product_id', FILTER_VALIDATE_INT);
$quantity  = filter_input(INPUT_POST, 'quantity', FILTER_VALIDATE_INT);

if ($productId === false || $productId === null ||
    $quantity === false || $quantity === null ||
    $productId < 1 || $quantity < 1 || $quantity > 99) {
    http_response_code(422);
    exit('Invalid cart data.');
}

if (isset($_SESSION['user_id'])) {
    addItemToUserCart($pdo, (int) $_SESSION['user_id'], $productId, $quantity);
} else {
    $_SESSION['cart'] ??= [];
    $_SESSION['cart'][$productId] =
        ($_SESSION['cart'][$productId] ?? 0) + $quantity;
}

header('Location: /cart.php', true, 303);
exit;

addItemToUserCart() represents your database-cart operation; it should use PDO prepared statements and enforce the store’s quantity limits. A 303 See Other redirect implements Post/Redirect/Get: refreshing the cart page will not resubmit the add-item POST.

Use CSRF tokens on cart mutations and other state-changing forms. A session cookie does not, by itself, prevent cross-site request forgery. Generate an unpredictable token with random_bytes(32), include it in the form, and check it with hash_equals() on submission. See PHP’s session security guidance.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

2. Require login at checkout

Enforce authentication on the server for every checkout request—not with a hidden form field or client-side check. If checkout is the only destination, save that fixed internal path rather than accepting an arbitrary URL:

<?php
// checkout.php

declare(strict_types=1);
session_start();

if (!isset($_SESSION['user_id'])) {
    $_SESSION['return_to'] = '/checkout.php';
    header('Location: /login.php', true, 302);
    exit;
}

$userId = (int) $_SESSION['user_id'];
$cart = loadCartForUser($pdo, $userId);

if (!$cart || $cart['items'] === []) {
    header('Location: /cart.php', true, 303);
    exit;
}

// Re-read current products and validate the cart before rendering checkout.

If the application needs to preserve other destinations, accept only known route names or validate a local path. Never redirect to an arbitrary value such as https://example-attacker.com. A simple local-path check can reject protocol-relative URLs and control characters:

function safeInternalPath(?string $path): string
{
    if (!$path || $path[0] !== '/' || str_starts_with($path, '//') ||
        preg_match('/[rn]/', $path)) {
        return '/account.php';
    }

    return $path;
}

A route allowlist is stronger than accepting every local path. Do not treat the return destination as proof that the user is authorized to access it; the destination must perform its own access check.

3. Verify credentials and transition the session

Use a prepared query and PHP’s password API. On successful verification, regenerate the session ID before writing authenticated state:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
$stmt = $pdo->prepare(
    'SELECT id, password_hash FROM users WHERE email = ? LIMIT 1'
);
$stmt->execute([$email]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);

if (!$user || !password_verify($password, $user['password_hash'])) {
    $error = 'Invalid email or password.';
} else {
    // Apply your production session-transition policy here.
    session_regenerate_id(true);
    $_SESSION['user_id'] = (int) $user['id'];

    $returnTo = safeInternalPath($_SESSION['return_to'] ?? '/account.php');
    unset($_SESSION['return_to']);

    header('Location: ' . $returnTo, true, 303);
    exit;
}

Store password hashes created with password_hash(), and verify them with password_verify(); do not store plaintext passwords or invent a separate salt scheme. See the PHP password API and password_hash().

Regenerating the ID at login is important because authentication raises the session’s privileges. PHP recommends ID regeneration at privilege changes, and OWASP discusses pre-login session fixation in the context of shopping carts. See PHP’s session security guidance and OWASP’s session-fixation test guidance.

session_regenerate_id(true) is a common concise tutorial pattern, not a universal production prescription. PHP warns that immediately deleting the old session can cause problems with concurrent requests or unstable connections. Follow the current PHP documentation and design the transition for your session handler and application.

4. Merge the guest cart without losing it

Decide what should happen when the account already has a cart. A common policy is to sum matching product quantities, subject to purchase limits and current stock. Other valid policies are to prefer the existing account cart, replace it with the guest cart, or ask the customer which to keep. Make the choice explicit so a login does not silently discard items.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For a merge, read guest items before changing session state. Perform database changes in a transaction, check that each product is still purchasable, and clear the guest cart only after the merge commits. The following is an outline; the helper functions must use prepared queries and the database’s appropriate upsert syntax:

function mergeGuestCart(PDO $pdo, int $userId, array $guestItems): void
{
    $pdo->beginTransaction();

    try {
        foreach ($guestItems as $productId => $quantity) {
            $productId = filter_var($productId, FILTER_VALIDATE_INT);
            $quantity  = filter_var($quantity, FILTER_VALIDATE_INT);

            if (!$productId || !$quantity || $productId < 1 || $quantity < 1) {
                continue;
            }

            $product = findPurchasableProduct($pdo, $productId);
            if (!$product) {
                continue;
            }

            // Apply a documented cap and account for quantities already in the cart.
            $existing = getUserCartQuantity($pdo, $userId, $productId);
            $cap = min(99, (int) $product['stock']);
            $combined = min($existing + $quantity, $cap);

            if ($combined > 0) {
                setUserCartQuantity($pdo, $userId, $productId, $combined);
            }
        }

        $pdo->commit();
    } catch (Throwable $e) {
        $pdo->rollBack();
        throw $e;
    }
}

$guestItems = $_SESSION['cart'] ?? [];
mergeGuestCart($pdo, (int) $_SESSION['user_id'], $guestItems);
unset($_SESSION['cart']);

The example caps the combined quantity at both a site limit and current stock; a store may instead preserve the requested quantity and flag the line for customer action. Deleted, unavailable, or out-of-stock products should not become purchasable merely because they were once in a guest cart. Tell the customer when an item was removed or reduced rather than silently changing the order where that matters.

In a complete login handler, redirect to checkout only after the merge succeeds. If the database operation fails, retain the guest cart and show a recoverable error. Keep the merge idempotent where possible, and use database uniqueness constraints or atomic upserts to handle duplicate requests and simultaneous tabs. PDO supplies prepared statements and transaction methods; see the PDO documentation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

5. Load product details from the database

A cart should primarily carry product identifiers and quantities, for example [123 => 2]. Do not treat browser-submitted product names, prices, discounts, tax, shipping, stock, user IDs, or totals as authoritative. Even if a session cart is used, query the product table when rendering the cart. Escape text for HTML output:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
echo htmlspecialchars(
    $item['name'],
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);

echo number_format((float) $item['unit_price'], 2, '.', ',');

At checkout, fetch current prices and availability, validate quantities and purchase limits, apply discounts using server-side rules, and calculate tax and shipping. Recalculate when creating the order as well. Save an order snapshot of product name, unit price, tax, and totals so later catalog changes do not rewrite the purchase record. Inventory-sensitive order creation should use a transaction and an appropriate locking or consistency strategy for your database. OWASP’s payment-functionality testing guidance covers business-logic risks such as manipulated item IDs and quantities.

6. Choose session or database cart storage

Approach Good fit Trade-offs
Session-only guest cart Tutorials, prototypes, short-lived carts Can disappear when the session expires or cookies are cleared; not naturally shared across devices.
Database cart for signed-in users Persistent account carts, multiple devices, cart recovery Needs ownership checks, cleanup, merge rules, and concurrency handling.
Database cart tied to a guest token Longer-lived guest carts in larger stores Needs secure token handling, expiry, and cleanup.
Hybrid session and database Sites that accept guest carts and persist them after login Flexible, but requires careful migration and conflict handling.

For a simple account-cart schema, separate cart headers from lines. Adapt types, timestamps, and index rules to your database engine and policy:

CREATE TABLE carts (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NULL,
    session_token CHAR(64) NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'active',
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

CREATE TABLE cart_items (
    cart_id BIGINT UNSIGNED NOT NULL,
    product_id BIGINT UNSIGNED NOT NULL,
    quantity INT UNSIGNED NOT NULL,
    PRIMARY KEY (cart_id, product_id),
    FOREIGN KEY (cart_id) REFERENCES carts(id)
);

Enforce your one-active-cart policy with constraints appropriate to your database. Nullable unique-key behavior differs between database engines, so do not assume a generic index declaration will enforce identical rules everywhere. A smaller application may use a simpler cart-items table, but it still needs to associate every cart with either a validated guest token or an authenticated user and enforce ownership on reads and writes.

Security and recovery checklist

  • Start the session on every request that reads or changes cart or authentication state; send no output before session headers.
  • Use HTTPS and review cookie attributes, session strict mode, idle timeouts, and session storage configuration.
  • Regenerate the session ID when login elevates privileges; do not put session IDs in URLs.
  • Validate IDs and quantities, use PDO prepared statements, and check cart ownership on every account-cart operation.
  • Protect state-changing forms with CSRF defenses.
  • Use a fixed or allowlisted internal return destination; do not permit open redirects.
  • Keep the guest cart until its database merge succeeds; report unavailable items or quantity changes clearly.
  • Make order creation idempotent and recalculate price, tax, shipping, and inventory server-side.
  • Do not store passwords or payment-card details in the session.

If the cart disappears after login, check that every relevant page starts the session, that output has not already been sent, and that the browser retains the same cookie across hostname and HTTP/HTTPS changes. Also check PHP’s session-save permissions, session lifetime, and whether login code destroys or overwrites session data. If the cart is in a database, inspect whether the merge failed before cleanup and whether reads are scoped to the authenticated user. For temporary local debugging, var_dump(session_status(), session_id(), $_SESSION); can reveal state; remove such output from production because it can expose sensitive session data.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Payment comes after cart validation

Once checkout has a validated server-side cart, create payment data on the server from that cart—not from a browser-submitted total. A hosted checkout service can handle the payment page, but the application still owns cart validation, order state, and payment confirmation. For example, Stripe documents server-created Checkout Sessions; do not treat a customer’s return to your site as proof of payment. Confirm payment using the provider’s server-side status and, where appropriate, webhook events.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.