Per gestire il carrello della nostra applicazione abbiamo innanzitutto bisogno di un metodo per validare le richieste HTTP fatte alla pagina del carrello. Creeremo il seguente metodo nella classe Validator
:
class Validator {
public static function isCartAddRequest($id, $qty) {
return (filter_var(intval($id), FILTER_VALIDATE_INT) && filter_var(intval($qty), FILTER_VALIDATE_INT));
}
}
Stiamo verificando che sia l'ID del prodotto che la quantità siano numeri interi validi.
Aggiunta di prodotti al carrello
Possiamo definire la action di aggiunta al carrello nella classe principale Shop
:
public function addToCart() {
if(Validator::isCartAddRequest($_REQUEST['id'], $_REQUEST['quantity'])) {
$id = $this->database->escape($_REQUEST['id']);
$quantity = $_REQUEST['quantity'];
if($this->database->exists('SELECT * FROM products WHERE id = ' . $id)) {
$product = $this->database->select("SELECT * FROM products WHERE id = $id");
$qty = (int) $quantity;
$cart_product = new Product($product[0]['id'], $product[0]['title'], $product[0]['description'], floatval($product[0]['price']), $product[0]['manufacturer'], $product[0]['image'], $product[0]['slug']);
if(!isset($_SESSION['cart'])) {
$cart = new Cart();
$cart->addToCart($cart_product, $qty);
$session_cart = ['items' => $cart->getItems(), 'total' => $cart->getTotal()];
Session::setItem('cart', $session_cart, true);
} else {
$sess_cart = unserialize($_SESSION['cart']);
$cart = new Cart();
$cart->setItems($sess_cart['items']);
$cart->setTotal($sess_cart['total']);
$cart->addToCart($cart_product, $qty);
$session_cart_upd = ['items' => $cart->getItems(), 'total' => $cart->getTotal()];
Session::setItem('cart', $session_cart_upd, true);
}
Router::redirect(SITE_URL . 'cart/');
} else {
Router::redirect(SITE_URL);
}
} else {
Router::redirect(SITE_URL);
}
}
La logica è semplice: se la richiesta è valida procediamo all'aggiunta del prodotto, altrimenti effettuiamo un redirect sulla home page. Stiamo usando l'array globale $_REQUEST
perché l'aggiunta al carrello può avvenire sia tramite link che tramite form, ossiavcon i metodi GET o POST.
Se il prodotto esiste nel database, istanziamo la classe Product
con i dati ottenuti tramite l'ID del prodotto e quindi se il carrello non è presente nella sessione, aggiungiamo il prodotto al carrello con la quantità prima di aggiornare il totale e salvare il carrello serializzato nella sessione.
Se invece il carrello è già presente nella sessione, dobbiamo prima deserializzarlo e quindi ripetere la stessa procedura vista in precedenza. Infine, se l'aggiunta ha avuto successo, effettuiamo il redirect alla pagina del carrello che ha la seguente view:
<?php if(!$is_empty_cart): ?>
<form action="/cart" method="post" id="form-cart">
<table class="table table-bordered">
<thead>
<tr>
<th scope="col" colspan="2"><?= $locale['form']['product']; ?></th>
<th scope="col"><?= $locale['form']['price']; ?></th>
<th scope="col"><?= $locale['form']['quantity']; ?></th>
<th scope="col" colspan="2"><?= $locale['form']['subtotal']; ?></th>
</tr>
</thead>
<tbody>
<?php foreach($items as $item): ?>
<tr>
<td><img src="<?= $item['image']; ?>" alt="" class="img-thumbnail"></td>
<td><a href="<?= $item['link']; ?>"><?= $item['name']; ?></a></td>
<td><?= $item['price']; ?></td>
<td><input type="number" name="quantity[]" step="1" min="1" class="form-control qty" value="<?= $item['quantity']; ?>-<?= $item['id']; ?>"></td>
<td><?= $item['subtotal']; ?></td>
<td><a href="/cart/?remove-item=<?= $item['id']; ?>" class="text-danger remove-cart-item">×</a></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<td colspan="6">
<b><?= $locale['form']['total']; ?>:</b> <?= $cart_total; ?>
</td>
</tfoot>
</table>
<div class="clearfix mt-5">
<button type="submit" name="update-cart" value="1" class="btn btn-success float-left"><?= $locale['buttons']['update']; ?></button>
<a href="/checkout/" class="btn btn-success float-right"><?= $locale['buttons']['proceed']; ?></a>
<a href="/" class="btn btn-success float-right mr-5"><?= $locale['buttons']['continue']; ?></a>
</div>
</form>
<?php else: ?>
<p class="alert alert-info"><?= $locale['form']['empty']; ?></p>
<?php endif; ?>
Nella tabella principale seguiamo la pratica collaudata dell'associazione delle quantità ai prodotti tramite la sintassi []
nei campi di input che di fatto crea all'atto dell'invio del form un array associativo tra ID del prodotto e quantità corrispondente.
La action che controlla questa view è la seguente:
public function cart() {
if(Session::hasItem('cart')) {
$sess_cart = Session::getItem('cart', true);
$is_empty_cart = (count($sess_cart['items']) == 0);
$cart_total = Templater::total($sess_cart['total']);
$items = Templater::cart($sess_cart['items']);
$front_cart = Templater::frontCart();
Render::view('cart', ['is_empty_cart' => $is_empty_cart,
'cart_total' => $cart_total, 'items' => $items, 'front_cart' => $front_cart]);
} else {
Router::redirect(SITE_URL);
}
}
Visualizziamo la pagina solo se il carrello è presente nella sessione. La classe Templater
è una classe di utility che a partire da dati grezzi restituisce un output sotto forma di frammenti HTML. A livello didattico il metodo più interessante di questa classe è Templater::total()
:
public static function total($value) {
return money_format('%.2n', floatval($value));
}
La funzione money_format()
lavora unitamente alla localizzazione ottenuta con setlocale()
: la sua particolarità sta nel fatto che la stringa indicante la valuta cambierà a seconda del paese. In Italia e dunque in Europa sarà EU
mentre ad esempio negli USA sarà USD
.
Rimozione di prodotti dal carrello
La rimozione di prodotti dal carrello è invece gestita modificando la action vista in precedenza utilizzando un metodo privato:
private function removeFromCart() {
$id = $_GET['remove-item'];
$prod_id = $this->database->escape($id);
$product = $this->database->select("SELECT * FROM products WHERE id = $prod_id");
$cart_product = new Product($product[0]['id'], $product[0]['title'], $product[0]['description'], floatval($product[0]['price']), $product[0]['manufacturer'], $product[0]['image'], $product[0]['slug']);
$sess_cart = Session::getItem('cart', true);
$cart = new Cart();
$cart->setItems($sess_cart['items']);
$cart->setTotal($sess_cart['total']);
$cart->removeFromCart($cart_product);
$session_cart_upd = ['items' => $cart->getItems(), 'total' => $cart->getTotal()];
Session::setItem('cart', $session_cart_upd, true);
}
public function cart() {
if(Session::hasItem('cart')) {
if(isset($_GET['remove-item'])) {
$this->removeFromCart();
}
$sess_cart = Session::getItem('cart', true);
$is_empty_cart = (count($sess_cart['items']) == 0);
$cart_total = Templater::total($sess_cart['total']);
$items = Templater::cart($sess_cart['items']);
$front_cart = Templater::frontCart();
Render::view('cart', ['is_empty_cart' => $is_empty_cart,
'cart_total' => $cart_total, 'items' => $items, 'front_cart' => $front_cart]);
} else {
Router::redirect(SITE_URL);
}
}
La rimozione avviene prima del rendering della view della pagina in modo sincrono e sequenziale rispetto alla routine che la segue.
Aggiornamento del carrello
Per l'aggiornamento del carrello la procedura è simile, ma dobbiamo anche considerare le quantità dei prodotti:
private function removeFromCart() {
//...
}
private function updateCart() {
$qtys = $_POST['quantity'];
$sess_cart = Session::getItem('cart', true);
$cart = new Cart();
$cart->setItems($sess_cart['items']);
$cart->setTotal($sess_cart['total']);
foreach($qtys as $part) {
$vars = explode('-', $part);
$id = $p[1];
$qty = (int) $p[0];
$prod_id = $this->database->escape($id);
$product = $this->database->select("SELECT * FROM products WHERE id = $prod_id");
$cart_product = new Product($product[0]['id'], $product[0]['title'], $product[0]['description'], floatval($product[0]['price']), $product[0]['manufacturer'], $product[0]['image'], $product[0]['slug']);
$cart->updateCart($cart_product, $qty);
}
$session_cart_upd = ['items' => $cart->getItems(), 'total' => $cart->getTotal()];
Session::setItem('cart', $session_cart_upd, true);
}
public function cart() {
if(Session::hasItem('cart')) {
if(isset($_GET['remove-item'])) {
$this->removeFromCart();
}
if(isset($_POST['update-cart']) && intval($_POST['update-cart']) == 1) {
$this->updateCart();
}
$sess_cart = Session::getItem('cart', true);
$is_empty_cart = (count($sess_cart['items']) == 0);
$cart_total = Templater::total($sess_cart['total']);
$items = Templater::cart($sess_cart['items']);
$front_cart = Templater::frontCart();
Render::view('cart', ['is_empty_cart' => $is_empty_cart,
'cart_total' => $cart_total, 'items' => $items, 'front_cart' => $front_cart]);
} else {
Router::redirect(SITE_URL);
}
}
L'array che abbiamo creato dal form tramite la coppia attributo/valore name="quantity[]"
ha la particolarità di avere nei valori dell'attributo value
sia la quantità che l'ID di ciascun prodotto con cui poi possiamo operare in un loop per l'aggiornamento del carrello. Tali valori sono separati da un trattino.