E5 - Sample Solution
Tags: php
Published: 2020-04-15 19:40:09

The files are viewable on this page directly, or on my [Github](https://github.com/herobank110/PHP-Tutorial).

Files in solution:
* [_header.php](#_header)
* [cart.php](#cart)
* [db_commands.sql](#db_commands)
* [edit_cart.php](#edit_cart)
* [index.php](#index)
* [on_checkout.php](#on_checkout)
* [order_confirm.php](#order_confirm)
* [style.css](#style)

Parent topic: [Example 5](https://dev.to/herobank110/exercise-5-shopping-facility-443n)

## _header.php <a name="_header"></a>
```php
<header>
<?php
function getCartCount()
{
    $cart = json_decode($_COOKIE["cart"] ?? "{}", true);
    $cartCount = 0;
    foreach ($cart as $productID => $quantity)
        $cartCount += $quantity;
    return $cartCount;
}
?>


    <a class="typography headline" href="index.php">Neat Treats</a>
    <a class="typography headline" href="index.php">Products</a>
    <a class="typography headline" href="cart.php">Cart (<?=getCartCount()?>)</a>
</header>
```

## cart.php <a name="cart"></a>
```php
<html>
    <head> <link rel="stylesheet" href="style.css"> </head>
    <body>
        <?php include("_header.php"); ?>
        <main>
            <h2 class="typography subhead">Cart Page</h2>


            <div class="list-grid cart-list-grid">
                <?php
                $cart = json_decode($_COOKIE["cart"] ?? "{}", true);


                $databaseLink = new mysqli("localhost", "root", "", "NeatTreats");
                foreach ($cart as $productID => $quantity) {
                    $result = $databaseLink->query(
                        "SELECT Name FROM Product WHERE ProductID=$productID;"
                    );
                    if (empty($databaseLink->error) && $result->num_rows > 0) {
                        $row = $result->fetch_object();
                        $productName = $row->Name;
                    } else {
                        $productName = "INVALID ID";
                    }
                    echo "<span class='typography body'>{$row->Name}</span>";
                    echo "<span class='typography body'> x$quantity</span>";
                    $editUrl = "edit_cart.php?id=$productID";
                    echo "<a class='typography body' href='$editUrl&type=add'>Add</a>";
                    echo "<a class='typography body' href='$editUrl&type=sub'>Sub</a>";
                    echo "<a class='typography body' href='$editUrl&type=rem'>Remove</a>";
                }
                $databaseLink->close();
                ?>
            </div>


            <form action="on_checkout.php" method="post">
                <div class="list-grid checkout-list-grid">
                    <label class='typography body'>Email Address:</label> <input name="email">
                    <label class='typography body'>Card Number:</label> <input name="card_num">
                    <label class='typography body'>Expiry Month:</label>
                    <select name="expiry_month">
                        <option selected disabled></option>
                        <?php
                        for ($i=1; $i < 13; $i++)
                            echo "<option value=$i>". date("F", 3600 * 24 * 28 * $i) ."</option>";
                        ?>
                    </select>
                    <label class='typography body'>Expiry Year:</label> <input name="expiry_year">
                    <div> <button>Checkout</button> </div>
                    <span class="error">
                        <?php
                        if (isset($_COOKIE["checkout_error"])) {
                            // Output and expire the checkout error cookie.
                            echo $_COOKIE["checkout_error"];
                            setcookie("checkout_error", "", time() - 3600);
                        }
                        ?>
                    </span>
                </div>
            </form>
        </main>
    </body>
</html>
```

## db_commands.sql <a name="db_commands"></a>
```sql
DROP DATABASE NeatTreats;
CREATE DATABASE NeatTreats;
USE NeatTreats;


CREATE TABLE Cart (
    CartID INT auto_increment,
    ProductID INT,
    Quantity INT,
    PRIMARY KEY (CartID, ProductID)
);


CREATE TABLE Product (
    ProductID INT auto_increment,
    Name VARCHAR(32),
    Description VARCHAR(255),
    Price FLOAT,
    PRIMARY KEY (ProductID)
);


INSERT INTO Product
    (ProductID, Name, Description, Price)
VALUES
    (1, 'Vanilla Cake', 'Tasty vanilla flavoured sponge cake', 12.34),
    (2, 'Chocolate Cake', 'Scrumptious chocolate flavoured sponge cake', 32.14),
    (3, 'Strawberry Cake', 'Yummy strawberry flavoured sponge cake', 42.35);


ALTER TABLE Cart ADD FOREIGN KEY (ProductID) REFERENCES Product (ProductID);
```

## edit_cart.php <a name="edit_cart"></a>
```php
<?php 
$productID = $_GET["id"];
$editType = $_GET["type"];
$redirectUrl = $_GET["redirect_url"] ?? "cart.php";
$cart = json_decode($_COOKIE["cart"] ?? "{}", true);


switch ($editType) {
case "add":
    // Increment quantity of already added product.
    if (isset($cart[$productID])) $cart[$productID]++;
    // Otherwise put 1 quantity of product in cart.
    else $cart[$productID] = 1;
    break;
case "sub":
    if (!isset($cart[$productID])) break;
    // Decrement quantity of already added product
    $cart[$productID]--;
    if ($cart[$productID] <= 0)
        // If quantity is now 0, remove from cart.
        unset($cart[$productID]);
    break;
case "rem":
    unset($cart[$productID]);
    break;
}


setcookie("cart", json_encode($cart), time() + 3600 * 24 * 10, "/");
header("Location: $redirectUrl");


?>
```

## index.php <a name="index"></a>
```php
<html>
    <head> <link rel="stylesheet" href="style.css"> </head>
    <body>
        <?php include("_header.php"); ?>
        <main>
            <h2 class="typography subhead">Products Page</h2>
            
            <div class="list-grid product-list-grid">
                <?php 
                    $databaseLink = new mysqli("localhost", "root", "", "NeatTreats");
                    $result = $databaseLink->query("SELECT * FROM Product LIMIT 200;");
                    if (empty($databaseLink->error)) {
                        while ($row = $result->fetch_object()) {
                            echo "<span class='typography body'>#{$row->ProductID}</span>";
                            echo "<img class='product-thumbnail' width='50' height='50' ";
                            echo "src='cake_images/cake_{$row->ProductID}.png' alt='cake image'>";
                            echo "<div>";
                            echo "<span class='typography body'>{$row->Name}</span>";
                            echo "<br>";
                            echo "<a class='typography body' href='edit_cart.php";
                            echo "?id={$row->ProductID}&type=add&redirect_url=index.php'>";
                            echo "Add to cart</a>";
                            echo "</div>";
                        }
                    }
                    $databaseLink->close();
                ?>
            </div>
        </main>
    </body>
</html>
```

## on_checkout.php <a name="on_checkout"></a>
```php
<?php
// Function definitions


/** Sanitizes and validates checkout form input
 * 
 * @param array $inputArray $_POST or $_GET array. Will be sanitized in place.
 * @param mysqli $databaseLink Link to sanitize SQL injections. If null, SQL
 * injections will not be protected.
 * @return boolean Whether all input is valid.
 */
function isCheckoutInputValid(&$inputArray, mysqli $databaseLink=null): bool
{
    foreach ($inputArray as $name => $input) {
        $input = strip_tags($input);
        if ($databaseLink !== null) $input = $databaseLink->real_escape_string($input);
        // Propagate changes back into array.
        $inputArray[$name] = $input;
    }


    $email = $inputArray["email"] ?? "";
    $cardNum = $inputArray["card_num"] ?? "";
    $expiryMonth = $inputArray["expiry_month"] ?? "";
    $expiryYear = $inputArray["expiry_year"] ?? "";


    // This validation example will not save each individual error. Refer
    // to example 3 for making specific error messages.
    return (
        !empty($email) // presence check
        && filter_var($email, FILTER_VALIDATE_EMAIL) // format: x@y.z
        && !empty($cardNum) // presence
        && (int)$cardNum != null // type: int (Also fails for $cardNum = "0")
        && (int)$cardNum > 0 // range: greater than 0
        && strlen($cardNum) >= 12 // length: between 12 and 16
        && strlen($cardNum) <= 16
        && !empty($expiryMonth) // presence
        && (int)$expiryMonth != null // type: int
        && (int)$expiryMonth >= 1 // range: between 1 and 12
        && (int)$expiryMonth <= 12
        && !empty($expiryYear) // presence
        && (int)$expiryYear != null // type: int
        && (int)$expiryYear >= 2020 // range: between 2020 and 3000
        && (int)$expiryYear <= 3000
    );
}


/** Save the cart into the database via the given link. */
function saveCartToDatabase(array $cart, mysqli $databaseLink): bool
{
    // Can't checkout with an empty cart!
    if (count($cart) == 0) return false;


    // Get the next valid auto increment CartID
    $databaseLink->query("INSERT INTO Cart (ProductID, Quantity) VALUES (1, 0);");
    if ($databaseLink->errno != 0) return false;
    $nextCartID = $databaseLink->insert_id;
    $databaseLink->query("DELETE FROM Cart WHERE CartID=$nextCartID;");
    if ($databaseLink->errno != 0) return false;


    // Add the cart rows to the database.
    $saveCartQuery = "INSERT INTO Cart (CartID, ProductID, Quantity) VALUES ";
    $isFirst = true;
    foreach ($cart as $productID => $quantity) {
        if ($isFirst) $isFirst = false;
        else $saveCartQuery .= ", ";
        $saveCartQuery .= "($nextCartID, $productID, $quantity)";
    }
    $saveCartQuery .= ";";


    $databaseLink->query($saveCartQuery);
    if ($databaseLink->errno != 0) return false;
    return true;
}


/** Send an invoice to the customer about the specified order. */
function sendInvoice(array $cart, array $inputArray): bool
{
    // Extract inputs from input array. Should have already been validated.
    $customerEmail = $inputArray["email"];
    $cardNum = $inputArray["card_num"];
    $expiryMonth = $inputArray["expiry_month"];
    $expiryYear = $inputArray["expiry_year"];


    $emailSubject = "Invoice of your order from Neat Treats";
    $emailHeaders = (
        "From: neattreats.sender@gmail.com\r\n" .
        "Content-Type: text/html; charset=ISO-8859-1\r\n"
    );


    $productsGrid = "";
    foreach($cart as $productID => $quantity)
        $productsGrid .= "<p>Product #$productID x$quantity</p>";


    $emailBody = (
        "<html>" .
            "<body>" .
                "<h2>Order Confirmation</h2>" .
                "<p>Thanks for your order! We hope you enjoy it a lot.</p>" .
                
                "<h4>Products:</h4>" .
                "<div style='padding-left:10px'>" .
                    $productsGrid .
                "</div>" .
                
                "<h4>Payment Method:</h4>" .
                "<div style='padding-left:10px'>" .
                    "<p>Card Number: $cardNum</p>" .
                    "<p>Expires: $expiryMonth / $expiryYear</p>" .
                "</div>" .
            "</body>" .
        "</html>"
    );
    return mail($customerEmail, $emailSubject, $emailBody, $emailHeaders);
}


// Performs actual script when page is opened:
(function () {
    $cart = json_decode($_COOKIE["cart"] ?? "{}", true);
    $databaseLink = new mysqli("localhost", "root", "", "NeatTreats");


    $allSuccess = false;
    $error = "";
    if (count($cart) > 0)
        if (isCheckoutInputValid($_POST))
            if (saveCartToDatabase($cart, $databaseLink))
                if (sendInvoice($cart, $_POST))
                    $allSuccess = true;
                else $error = "Couldn't send invoice";
            else $error = "Couldn't save to database";
        else $error = "Invalid checkout input";
    else $error = "Cart is empty";


    $databaseLink->close();


    if ($allSuccess) {
        setcookie("cart", "", time() - 3600, "/");
        header("Location: order_confirm.php");
    } else {
        setcookie("checkout_error", $error, time() + 3600, "cart.php");
        header("Location: cart.php");
    }
})();
?>
```

## order_confirm.php <a name="order_confirm"></a>
```php
<html><body>
    <h2>Order Confirmation</h2>
    <p>Thanks for your order!</p>
</body></html>
```

## style.css <a name="style"></a>
```css
.typography { font-family: Arial, Helvetica, sans-serif; }
.typography.headline { font-size: 24; }
.typography.subhead { font-size: 20; font-weight: lighter; }
.typography.body { font-size: 14; }


header {
    display: inline-grid; grid-template-columns: 2fr 1fr 1fr;
    place-items: center;
    width: 420px;
}


header>a { text-decoration: none; }
header>a:active, header>a:visited { color: orange; }
header>a:hover { color: darkorange; }


main { margin: 30px 10px; }


.list-grid {
    display: inline-grid; row-gap: 5px; column-gap: 5px;
    margin-left: 10px; border-left: 1px solid grey; padding-left: 3px;
}
.product-list-grid { grid-template-columns: auto auto auto; }
.cart-list-grid { grid-template-columns: auto auto auto auto auto; margin-bottom: 20px; }
.checkout-list-grid { grid-template-columns: auto auto; }


.product-thumbnail { border-radius: 4px; border: 1px solid lightgrey; }
.error { color: #e22828; }
```

Parent topic: [Example 5](https://dev.to/herobank110/exercise-5-shopping-facility-443n)