Compare commits
8 Commits
922b29519a
...
master
Author | SHA1 | Date | |
---|---|---|---|
17e5f0d5dc | |||
7742235180 | |||
b9bf831001 | |||
b09e8fff28 | |||
6ce3d64649 | |||
16b4d686e3 | |||
7d2ae0b2d2 | |||
40b7d51b39 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.phan/
|
@@ -1,16 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="index.css"/>
|
||||
<title>Product Add</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 id="title">Product Add</h1>
|
||||
<div id="buttons">
|
||||
<a href="/"><button>Save</button></a>
|
||||
<a href="/"><button>Cancel</button></a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@@ -1,3 +1,5 @@
|
||||
DROP DATABASE IF EXISTS `products`;
|
||||
|
||||
CREATE DATABASE `products`;
|
||||
|
||||
USE `products`;
|
||||
@@ -9,7 +11,7 @@ CREATE TABLE `product` (
|
||||
`sku` varchar(100) NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `sku` (`sku`)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE `book` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
@@ -18,16 +20,16 @@ CREATE TABLE `book` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `book_UN` (`product_id`),
|
||||
CONSTRAINT `book_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE `dvd` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
`product_id` int unsigned NOT NULL,
|
||||
`size` int unsigned NOT NULL,
|
||||
`size` float NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `dvd_UN` (`product_id`),
|
||||
CONSTRAINT `dvd_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE `furniture` (
|
||||
`id` int unsigned NOT NULL AUTO_INCREMENT,
|
||||
@@ -38,5 +40,4 @@ CREATE TABLE `furniture` (
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `furniture_UN` (`product_id`),
|
||||
CONSTRAINT `furniture_FK` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`)
|
||||
)
|
||||
|
||||
);
|
20
index.html
20
index.html
@@ -1,20 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="index.css"/>
|
||||
<title>Product List</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 id="title">Product List</h1>
|
||||
<div id="buttons">
|
||||
<a href="add-product"><button>ADD</button></a>
|
||||
<button id="delete-product-btn">MASS DELETE</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="products">
|
||||
</div>
|
||||
</body>
|
||||
<script src="index.js"></script>
|
||||
</html>
|
45
index.js
45
index.js
@@ -1,45 +0,0 @@
|
||||
const productsList = document.getElementById('products');
|
||||
|
||||
const loadItems = () => {
|
||||
const xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onload = function() {
|
||||
const products = JSON.parse(this.responseText);
|
||||
|
||||
const boxes = products.map(product =>
|
||||
`<div class="product">
|
||||
<input type="checkbox" class="delete-checkbox" value="${product.id}">
|
||||
<p>
|
||||
${product.sku}<br>
|
||||
${product.name}<br>
|
||||
${product.price} $<br>
|
||||
${product.attribute}
|
||||
</p>
|
||||
</div>`
|
||||
)
|
||||
|
||||
productsList.innerHTML = boxes.join('\n');
|
||||
}
|
||||
|
||||
xhttp.open('GET', 'products', true);
|
||||
xhttp.send();
|
||||
}
|
||||
loadItems();
|
||||
|
||||
const deleteSelected = () => {
|
||||
const checkboxes = document.querySelectorAll('input[class="delete-checkbox"]:checked');
|
||||
let values = [];
|
||||
checkboxes.forEach(checkbox => values.push(checkbox.value));
|
||||
|
||||
const xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onload = function() {
|
||||
loadItems();
|
||||
}
|
||||
|
||||
xhttp.open('DELETE', 'products', true);
|
||||
xhttp.send();
|
||||
}
|
||||
|
||||
const deleteButton = document.getElementById('delete-product-btn');
|
||||
deleteButton.addEventListener('click', deleteSelected);
|
13
index.php
13
index.php
@@ -1,17 +1,22 @@
|
||||
<?php
|
||||
require 'autoload.php';
|
||||
ini_set('display_errors', true);
|
||||
ini_set('error_log', '/tmp/php.log');
|
||||
|
||||
use ProductList\Http\Request;
|
||||
use ProductList\Http\RequestHandler;
|
||||
use ProductList\Http\Route;
|
||||
|
||||
$request = new Request($_SERVER);
|
||||
$request = new Request($_SERVER, $_GET, $_POST);
|
||||
$handler = new RequestHandler($request);
|
||||
|
||||
$handler->registerRoutes([
|
||||
new Route('GET', 'products', ['ProductList\View\Product', 'listAll']),
|
||||
new Route('GET', 'add-product', function() { readfile('add-product.html'); }),
|
||||
new Route('GET', '', function() { readfile('index.html'); }),
|
||||
new Route('GET', 'test', ['ProductList\View\Product', 'test']),
|
||||
new Route('GET', 'product', ['ProductList\View\Product', 'get']),
|
||||
new Route('DELETE', 'product', ['ProductList\View\Product', 'delete']),
|
||||
new Route('POST', 'product', ['ProductList\View\Product', 'post']),
|
||||
new Route('GET', 'add-product', function() { readfile('static/add-product.html'); }),
|
||||
new Route('GET', '', function() { readfile('static/index.html'); }),
|
||||
]);
|
||||
|
||||
$handler->handle();
|
||||
|
4
src/Exception/DuplicateException.php
Normal file
4
src/Exception/DuplicateException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace ProductList\Exception;
|
||||
|
||||
class DuplicateException extends \Exception {}
|
11
src/Exception/InvalidFieldException.php
Normal file
11
src/Exception/InvalidFieldException.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace ProductList\Exception;
|
||||
|
||||
class InvalidFieldException extends \Exception
|
||||
{
|
||||
public function __construct($field, $code = 0, Throwable $previous = null)
|
||||
{
|
||||
http_response_code(400);
|
||||
parent::__construct("The field '$field' is invalid.", $code, $previous);
|
||||
}
|
||||
}
|
4
src/Exception/NotFoundException.php
Normal file
4
src/Exception/NotFoundException.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
namespace ProductList\Exception;
|
||||
|
||||
class NotFoundException extends \Exception {}
|
@@ -5,6 +5,8 @@ class Request
|
||||
{
|
||||
private $method;
|
||||
private $uri;
|
||||
private $queryParams;
|
||||
private $formParams;
|
||||
|
||||
public function getMethod()
|
||||
{
|
||||
@@ -16,9 +18,25 @@ class Request
|
||||
return $this->uri;
|
||||
}
|
||||
|
||||
public function __construct(array $params)
|
||||
public function getQueryParams()
|
||||
{
|
||||
$this->uri = basename($params['REQUEST_URI']);
|
||||
return $this->queryParams;
|
||||
}
|
||||
|
||||
public function getFormParams()
|
||||
{
|
||||
return $this->formParams;
|
||||
}
|
||||
|
||||
public function __construct(array $params, array $queryParams, array $formParams)
|
||||
{
|
||||
$uri_base = strtok($params['REQUEST_URI'], '?');
|
||||
$uri_base = trim(urldecode($uri_base), '/');
|
||||
$this->uri = explode('/', $uri_base);
|
||||
|
||||
$this->method = $params['REQUEST_METHOD'];
|
||||
|
||||
$this->queryParams = $queryParams;
|
||||
$this->formParams = $formParams;
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,12 @@ class RequestHandler
|
||||
{
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route->matches($this->request)) {
|
||||
$route->execute($this->request);
|
||||
try {
|
||||
$route->execute($this->request);
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
echo $e->getMessage();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,7 @@ class Route
|
||||
public function __construct(string $method, string $uri, array|\Closure $view)
|
||||
{
|
||||
$this->method = $method;
|
||||
$this->uri = $uri;
|
||||
$this->uri = explode('/', $uri);
|
||||
$this->view = $view;
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace ProductList\Model;
|
||||
|
||||
use ProductList\Exception\InvalidFieldException;
|
||||
|
||||
class Book extends Product
|
||||
{
|
||||
private $weight;
|
||||
@@ -14,6 +16,11 @@ class Book extends Product
|
||||
$variationId = null
|
||||
) {
|
||||
parent::__construct($sku, $name, $price, $productId, $variationId);
|
||||
|
||||
if (empty($weight) || !is_numeric($weight)) {
|
||||
throw new InvalidFieldException('Weight');
|
||||
}
|
||||
|
||||
$this->weight = $weight;
|
||||
}
|
||||
|
||||
@@ -24,8 +31,8 @@ class Book extends Product
|
||||
$row['name'],
|
||||
$row['price'],
|
||||
$row['weight'],
|
||||
$row['product_id'],
|
||||
$row['variation_id']
|
||||
$row['productId'],
|
||||
$row['variationId']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,8 +49,15 @@ class Book extends Product
|
||||
|
||||
private static function getSelectAllQuery() : string
|
||||
{
|
||||
return 'SELECT '.PRODUCT.'.*, '.BOOK.'.id as variation_id, size
|
||||
FROM '.PRODUCT.' LEFT JOIN '.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id';
|
||||
return '
|
||||
SELECT
|
||||
'.PRODUCT.'.*,
|
||||
'.BOOK.'.id as variationId,
|
||||
weight
|
||||
FROM
|
||||
'.PRODUCT.'
|
||||
LEFT JOIN
|
||||
'.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id';
|
||||
}
|
||||
|
||||
public function insert($conn = null) : int
|
||||
@@ -66,7 +80,24 @@ class Book extends Product
|
||||
$this->setVariationId($conn->insert_id);
|
||||
return $conn->insert_id;
|
||||
} else {
|
||||
throw new Exception("Unable to insert object");
|
||||
throw new \Exception("Unable to insert object");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($conn = null)
|
||||
{
|
||||
if ($conn === null) {
|
||||
$conn = Database::connect();
|
||||
}
|
||||
|
||||
$variationId = $this->getVariationId();
|
||||
$stmt = $conn->prepare('DELETE FROM '.BOOK.' WHERE id = ?');
|
||||
$stmt->bind_param('i', $variationId);
|
||||
|
||||
if ($stmt->execute() === false) {
|
||||
throw new \Exception("Unable to delete product with id '$variationId'");
|
||||
}
|
||||
|
||||
parent::delete($conn);
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace ProductList\Model;
|
||||
|
||||
use ProductList\Exception\InvalidFieldException;
|
||||
|
||||
class DVD extends Product
|
||||
{
|
||||
private $size;
|
||||
@@ -14,6 +16,11 @@ class DVD extends Product
|
||||
$variationId = null
|
||||
) {
|
||||
parent::__construct($sku, $name, $price, $productId, $variationId);
|
||||
|
||||
if (empty($size) || !is_numeric($size)) {
|
||||
throw new InvalidFieldException('Size');
|
||||
}
|
||||
|
||||
$this->size = $size;
|
||||
}
|
||||
|
||||
@@ -24,8 +31,8 @@ class DVD extends Product
|
||||
$row['name'],
|
||||
$row['price'],
|
||||
$row['size'],
|
||||
$row['product_id'],
|
||||
$row['variation_id']
|
||||
$row['productId'],
|
||||
$row['variationId']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,8 +49,15 @@ class DVD extends Product
|
||||
|
||||
private static function getSelectAllQuery() : string
|
||||
{
|
||||
return 'SELECT '.PRODUCT.'.*, '.DVD.'.id as variation_id, size
|
||||
FROM '.PRODUCT.' LEFT JOIN '.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id';
|
||||
return '
|
||||
SELECT
|
||||
'.PRODUCT.'.*,
|
||||
'.DVD.'.id as variationId,
|
||||
size
|
||||
FROM
|
||||
'.PRODUCT.'
|
||||
LEFT JOIN
|
||||
'.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id';
|
||||
}
|
||||
|
||||
public function insert($conn = null) : int
|
||||
@@ -60,13 +74,30 @@ class DVD extends Product
|
||||
$size = $this->getSize();
|
||||
|
||||
$stmt = $conn->prepare("INSERT INTO ".DVD." (product_id, size) VALUES (?, ?);");
|
||||
$stmt->bind_param('ii', $productId, $size);
|
||||
$stmt->bind_param('id', $productId, $size);
|
||||
|
||||
if ($stmt->execute() === true) {
|
||||
$this->setVariationId($conn->insert_id);
|
||||
return $conn->insert_id;
|
||||
} else {
|
||||
throw new Exception("Unable to insert object");
|
||||
throw new \Exception("Unable to insert object");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($conn = null)
|
||||
{
|
||||
if ($conn === null) {
|
||||
$conn = Database::connect();
|
||||
}
|
||||
|
||||
$variationId = $this->getVariationId();
|
||||
$stmt = $conn->prepare('DELETE FROM '.DVD.' WHERE id = ?');
|
||||
$stmt->bind_param('i', $variationId);
|
||||
|
||||
if ($stmt->execute() === false) {
|
||||
throw new \Exception("Unable to delete product with id '$variationId'");
|
||||
}
|
||||
|
||||
parent::delete($conn);
|
||||
}
|
||||
}
|
||||
|
@@ -8,15 +8,10 @@ define("FURNITURE", "furniture");
|
||||
|
||||
class Database
|
||||
{
|
||||
const SERVERNAME = "127.0.0.1";
|
||||
const DATABASE = "scandiweb";
|
||||
const USERNAME = "root";
|
||||
const PASSWORD = "root";
|
||||
|
||||
public static function connect()
|
||||
{
|
||||
$conn = new \mysqli(self::SERVERNAME, self::USERNAME, self::PASSWORD);
|
||||
$conn->select_db(self::DATABASE);
|
||||
$conn = new \mysqli(getenv('SERVERNAME'), getenv('USERNAME'), getenv('PASSWORD'));
|
||||
$conn->select_db(getenv('DATABASE'));
|
||||
return $conn;
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,8 @@
|
||||
<?php
|
||||
namespace ProductList\Model;
|
||||
|
||||
use ProductList\Exception\InvalidFieldException;
|
||||
|
||||
class Furniture extends Product
|
||||
{
|
||||
private $height;
|
||||
@@ -8,7 +10,7 @@ class Furniture extends Product
|
||||
private $length;
|
||||
|
||||
public function __construct(
|
||||
$SKU,
|
||||
$sku,
|
||||
$name,
|
||||
$price,
|
||||
$height,
|
||||
@@ -17,7 +19,20 @@ class Furniture extends Product
|
||||
$productId = null,
|
||||
$variationId = null
|
||||
) {
|
||||
parent::__construct($SKU, $name, $price, $productId, $variationId);
|
||||
parent::__construct($sku, $name, $price, $productId, $variationId);
|
||||
|
||||
if (empty($height) || !is_numeric($height)) {
|
||||
throw new InvalidFieldException('Height');
|
||||
}
|
||||
|
||||
if (empty($width) || !is_numeric($width)) {
|
||||
throw new InvalidFieldException('Width');
|
||||
}
|
||||
|
||||
if (empty($length) || !is_numeric($length)) {
|
||||
throw new InvalidFieldException('Length');
|
||||
}
|
||||
|
||||
$this->height = $height;
|
||||
$this->width = $width;
|
||||
$this->length = $length;
|
||||
@@ -32,8 +47,8 @@ class Furniture extends Product
|
||||
$row['height'],
|
||||
$row['width'],
|
||||
$row['length'],
|
||||
$row['product_id'],
|
||||
$row['variation_id']
|
||||
$row['productId'],
|
||||
$row['variationId']
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,9 +77,17 @@ class Furniture extends Product
|
||||
|
||||
private static function getSelectAllQuery() : string
|
||||
{
|
||||
return 'SELECT '.PRODUCT.'.*, '.FURNITURE.'.id as variation_id, size
|
||||
FROM '.PRODUCT.'
|
||||
LEFT JOIN '.FURNITURE.' ON '.PRODUCT.'.id = '.FURNITURE.'.product_id';
|
||||
return '
|
||||
SELECT
|
||||
'.PRODUCT.'.*,
|
||||
'.FURNITURE.'.id as variationId,
|
||||
height,
|
||||
width,
|
||||
length
|
||||
FROM
|
||||
'.PRODUCT.'
|
||||
LEFT JOIN '.FURNITURE.' ON
|
||||
'.PRODUCT.'.id = '.FURNITURE.'.product_id';
|
||||
}
|
||||
|
||||
public function insert($conn = null) : int
|
||||
@@ -92,7 +115,24 @@ class Furniture extends Product
|
||||
$this->setVariationId($conn->insert_id);
|
||||
return $conn->insert_id;
|
||||
} else {
|
||||
throw new Exception("Unable to insert object");
|
||||
throw new \Exception("Unable to insert object");
|
||||
}
|
||||
}
|
||||
|
||||
public function delete($conn = null)
|
||||
{
|
||||
if ($conn === null) {
|
||||
$conn = Database::connect();
|
||||
}
|
||||
|
||||
$variationId = $this->getVariationId();
|
||||
$stmt = $conn->prepare('DELETE FROM '.FURNITURE.' WHERE id = ?');
|
||||
$stmt->bind_param('i', $variationId);
|
||||
|
||||
if ($stmt->execute() === false) {
|
||||
throw new \Exception("Unable to delete product with id '$variationId'");
|
||||
}
|
||||
|
||||
parent::delete($conn);
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ trait Model
|
||||
{
|
||||
abstract public static function fromRow($row) : self;
|
||||
abstract public function insert($conn = null) : int; // should return id
|
||||
abstract public function delete($conn = null);
|
||||
abstract private static function getSelectAllQuery() : string;
|
||||
|
||||
public static function selectAll($conn = null) : array
|
||||
|
@@ -1,32 +1,50 @@
|
||||
<?php
|
||||
namespace ProductList\Model;
|
||||
|
||||
use ProductList\Exception\NotFoundException;
|
||||
use ProductList\Exception\InvalidFieldException;
|
||||
use ProductList\Exception\DuplicateException;
|
||||
|
||||
abstract class Product implements \JsonSerializable
|
||||
{
|
||||
use Model;
|
||||
private $variationId;
|
||||
private $SKU;
|
||||
private $sku;
|
||||
private $name;
|
||||
private $price;
|
||||
private $productId;
|
||||
|
||||
public function __construct(
|
||||
$SKU,
|
||||
$sku,
|
||||
$name,
|
||||
$price,
|
||||
$productId = null,
|
||||
$variationId = null
|
||||
) {
|
||||
if (empty($sku)) {
|
||||
throw new InvalidFieldException('SKU');
|
||||
}
|
||||
|
||||
if (empty($name)) {
|
||||
throw new InvalidFieldException('Name');
|
||||
}
|
||||
|
||||
if (empty($price) || !is_numeric($price)) {
|
||||
throw new InvalidFieldException('Price');
|
||||
}
|
||||
|
||||
$this->productId = $productId;
|
||||
$this->variationId = $variationId;
|
||||
$this->SKU = $SKU;
|
||||
|
||||
|
||||
$this->sku = $sku;
|
||||
$this->name = $name;
|
||||
$this->price = $price;
|
||||
}
|
||||
|
||||
public function getSKU()
|
||||
{
|
||||
return $this->SKU;
|
||||
return $this->sku;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
@@ -44,6 +62,11 @@ abstract class Product implements \JsonSerializable
|
||||
return $this->productId;
|
||||
}
|
||||
|
||||
public function getVariationId()
|
||||
{
|
||||
return $this->variationId;
|
||||
}
|
||||
|
||||
public function setVariationId($id)
|
||||
{
|
||||
$this->variationId = $id;
|
||||
@@ -56,28 +79,84 @@ abstract class Product implements \JsonSerializable
|
||||
|
||||
abstract public function getFormatedAttr();
|
||||
|
||||
public function delete($conn = null)
|
||||
{
|
||||
if ($conn === null) {
|
||||
$conn = Database::connect();
|
||||
}
|
||||
|
||||
$productId = $this->getProductId();
|
||||
$stmt = $conn->prepare('DELETE FROM '.PRODUCT.' WHERE id = ?');
|
||||
$stmt->bind_param('i', $productId);
|
||||
|
||||
if ($stmt->execute() === false) {
|
||||
throw new \Exception("Unable to delete product with id '$productId'");
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromId($id, $conn = null) : self
|
||||
{
|
||||
if ($conn === null) {
|
||||
$conn = Database::connect();
|
||||
}
|
||||
|
||||
$stmt = $conn->prepare(self::getSelectAllQuery().' WHERE '.PRODUCT.'.id = ?');
|
||||
$stmt->bind_param('i', $id);
|
||||
|
||||
if ($stmt->execute() === true) {
|
||||
$row = $stmt->get_result()->fetch_assoc();
|
||||
|
||||
if($row === null) {
|
||||
throw new NotFoundException("No product with id '$id'");
|
||||
}
|
||||
|
||||
return self::fromRow($row);
|
||||
} else {
|
||||
throw new \Exception("Unable to select object");
|
||||
}
|
||||
}
|
||||
|
||||
public static function fromRow($row) : self
|
||||
{
|
||||
if ($row['size'] !== null) {
|
||||
return DVD::fromRow($row);
|
||||
} elseif ($row['weight'] !== null) {
|
||||
return Book::fromRow($row);
|
||||
} elseif ($row['height'] !== null) {
|
||||
return Furniture::fromRow($row);
|
||||
} else {
|
||||
throw new Exception("Product without a type");
|
||||
switch ($row['productType']) {
|
||||
case 'dvd':
|
||||
return DVD::fromRow($row);
|
||||
case 'book':
|
||||
return Book::fromRow($row);
|
||||
case 'furniture':
|
||||
return Furniture::fromRow($row);
|
||||
default:
|
||||
throw new \Exception("Product without a type");
|
||||
}
|
||||
}
|
||||
|
||||
private static function getSelectAllQuery() : string
|
||||
{
|
||||
return 'SELECT '.PRODUCT.'.id as product_id,
|
||||
COALESCE('.DVD.'.id, '.BOOK.'.id, '.FURNITURE.'.id) as variation_id,
|
||||
name, sku, price, size, weight, width, height, length
|
||||
FROM '.PRODUCT.'
|
||||
LEFT JOIN '.DVD.' ON '.PRODUCT.'.id = '.DVD.'.product_id
|
||||
LEFT JOIN '.BOOK.' ON '.PRODUCT.'.id = '.BOOK.'.product_id
|
||||
LEFT JOIN '.FURNITURE.' ON '.PRODUCT.'.id = '.FURNITURE.'.product_id;';
|
||||
return '
|
||||
SELECT
|
||||
'.PRODUCT.'.id as productId,
|
||||
name,
|
||||
sku,
|
||||
price,
|
||||
size,
|
||||
weight,
|
||||
width,
|
||||
height,
|
||||
length,
|
||||
COALESCE('.DVD.'.id, '.BOOK.'.id, '.FURNITURE.'.id) as variationId,
|
||||
CASE
|
||||
WHEN '.DVD.'.id IS NOT NULL THEN "dvd"
|
||||
WHEN '.BOOK.'.id IS NOT NULL THEN "book"
|
||||
WHEN '.FURNITURE.'.id IS NOT NULL THEN "furniture"
|
||||
END as productType
|
||||
FROM
|
||||
'.PRODUCT.'
|
||||
LEFT JOIN '.DVD.' ON
|
||||
'.PRODUCT.'.id = '.DVD.'.product_id
|
||||
LEFT JOIN '.BOOK.' ON
|
||||
'.PRODUCT.'.id = '.BOOK.'.product_id
|
||||
LEFT JOIN '.FURNITURE.' ON
|
||||
'.PRODUCT.'.id = '.FURNITURE.'.product_id';
|
||||
}
|
||||
|
||||
public function insert($conn = null) : int
|
||||
@@ -96,11 +175,19 @@ abstract class Product implements \JsonSerializable
|
||||
);
|
||||
$stmt->bind_param('ssd', $SKU, $name, $price);
|
||||
|
||||
if ($stmt->execute() === true) {
|
||||
$this->setProductId($conn->insert_id);
|
||||
return $conn->insert_id;
|
||||
} else {
|
||||
throw new Exception("Unable to insert object");
|
||||
try {
|
||||
if ($stmt->execute() === true) {
|
||||
$this->setProductId($conn->insert_id);
|
||||
return $conn->insert_id;
|
||||
} else {
|
||||
throw new \Exception("Unable to insert object");
|
||||
}
|
||||
} catch (\mysqli_sql_exception $e) {
|
||||
if ($e->getCode() === 1062) {
|
||||
throw new DuplicateException('The provided SKU is already registered.');
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -3,11 +3,69 @@ namespace ProductList\View;
|
||||
|
||||
use ProductList\Http\Request;
|
||||
use ProductList\Model\Product as ProductModel;
|
||||
use ProductList\Exception\NotFoundException;
|
||||
|
||||
class Product
|
||||
class Product extends View
|
||||
{
|
||||
public static function listAll(Request $request)
|
||||
public static function get(Request $request)
|
||||
{
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(ProductModel::selectAll());
|
||||
}
|
||||
|
||||
public static function delete(Request $request)
|
||||
{
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
if (array_key_exists('id', $queryParams)) {
|
||||
$ids = explode(',', $queryParams['id']);
|
||||
$ids = array_map('intval', $ids);
|
||||
|
||||
foreach($ids as $id) {
|
||||
try {
|
||||
$product = ProductModel::fromId($id);
|
||||
|
||||
try {
|
||||
$product->delete();
|
||||
} catch (\Exception $e) {
|
||||
http_response_code(500);
|
||||
echo $e->getMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
} catch (NotFoundException $e) {
|
||||
http_response_code(404);
|
||||
echo "The selected(s) object(s) is(are) not available anymore.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
http_response_code(400);
|
||||
echo 'Missing parameter "id".';
|
||||
}
|
||||
}
|
||||
|
||||
public static function post(Request $request)
|
||||
{
|
||||
$params= $request->getFormParams();
|
||||
$expected = [
|
||||
'sku',
|
||||
'name',
|
||||
'price',
|
||||
'productType',
|
||||
'weight',
|
||||
'size',
|
||||
'height',
|
||||
'width',
|
||||
'length'
|
||||
];
|
||||
if (self::expectArgs($expected, $params)) {
|
||||
$params['productId'] = null;
|
||||
$params['variationId'] = null;
|
||||
|
||||
$product = ProductModel::fromRow($params);
|
||||
|
||||
$product->insert();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
22
src/View/View.php
Normal file
22
src/View/View.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace ProductList\View;
|
||||
|
||||
abstract class View
|
||||
{
|
||||
protected static function expectArgs(array $args, array $provided) {
|
||||
$keys = array_keys($provided);
|
||||
|
||||
$missing = array_diff($args, $keys);
|
||||
|
||||
if (count($missing) > 0) {
|
||||
http_response_code(400);
|
||||
|
||||
$missing = join("', '", $missing);
|
||||
|
||||
echo "Missing parameters '$missing'";
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
83
static/add-product.html
Normal file
83
static/add-product.html
Normal file
@@ -0,0 +1,83 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="static/stylesheet.css"/>
|
||||
<title>Product Add</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 id="title">Product Add</h1>
|
||||
<div id="buttons">
|
||||
<div class="header-button"><button type="submit" form="product_form" id="save">Save</button></div>
|
||||
<div class="header-button"><a href="/"><button>Cancel</button></a></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main">
|
||||
<form id="product_form">
|
||||
<div id="basic-info" class="form-section">
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="sku">SKU</label></td>
|
||||
<td><input type="text" id="sku" name="sku" required></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="name">Name</label></td>
|
||||
<td><input type="text" id="name" name="name" required></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="price">Price ($)</label></td>
|
||||
<td><input type="text" id="price" name="price" required></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div id="type-switcher" class="form-section">
|
||||
<label for="productType">Type Switcher</label>
|
||||
<select name="productType" id="productType">
|
||||
<option value="dvd">DVD</option>
|
||||
<option value="furniture">Furniture</option>
|
||||
<option value="book">Book</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="attribute" class="form-section">
|
||||
<div id="dvd" class="hidden">
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="size">Size (MB)</label></td>
|
||||
<td><input type="text" id="size" name="size"></td>
|
||||
</tr>
|
||||
</table>
|
||||
Please, provide size
|
||||
</div>
|
||||
<div id="furniture" class="hidden">
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="height">Height (CM)</label></td>
|
||||
<td><input type="text" id="height" name="height"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="width">Width (CM)</label></td>
|
||||
<td><input type="text" id="width" name="width"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="length">Length (CM)</label></td>
|
||||
<td><input type="text" id="length" name="length"></td>
|
||||
</tr>
|
||||
</table>
|
||||
Please, provide dimensions
|
||||
</div>
|
||||
<div id="book" class="hidden">
|
||||
<table>
|
||||
<tr>
|
||||
<td><label for="weight">Weight (KG)</label></td>
|
||||
<td><input type="text" id="weight" name="weight"></td>
|
||||
</tr>
|
||||
</table>
|
||||
Please, provide weight
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script src="static/add-product.js"></script>
|
||||
</html>
|
29
static/add-product.js
Normal file
29
static/add-product.js
Normal file
@@ -0,0 +1,29 @@
|
||||
let currentTypeForm = document.getElementById($('#productType').val());
|
||||
currentTypeForm.querySelectorAll('input').forEach(input => input.required = true);
|
||||
currentTypeForm.classList.remove('hidden');
|
||||
|
||||
$('#productType').on('change', e => {
|
||||
const newTypeForm = document.getElementById(e.target.value);
|
||||
|
||||
currentTypeForm.classList.add('hidden');
|
||||
newTypeForm.classList.remove('hidden');
|
||||
|
||||
currentTypeForm.querySelectorAll('input').forEach(input => input.required = false);
|
||||
newTypeForm.querySelectorAll('input').forEach(input => input.required = true);
|
||||
|
||||
currentTypeForm = newTypeForm;
|
||||
});
|
||||
|
||||
$('#product_form').on('submit', e=> {
|
||||
e.preventDefault();
|
||||
|
||||
$.ajax(
|
||||
'product',
|
||||
{
|
||||
method: 'POST',
|
||||
data: $('#product_form').serializeArray(),
|
||||
success: _ => window.location.href = '/',
|
||||
error: jqXHR => alert(jqXHR.responseText),
|
||||
}
|
||||
);
|
||||
});
|
21
static/index.html
Normal file
21
static/index.html
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<link rel="stylesheet" href="static/stylesheet.css"/>
|
||||
<title>Product List</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header">
|
||||
<h1 id="title">Product List</h1>
|
||||
<div id="buttons">
|
||||
<div class="header-button"><a href="add-product"><button>ADD</button></a></div>
|
||||
<div class="header-button"><button id="delete-product-btn">MASS DELETE</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="products">
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<script src="static/index.js"></script>
|
||||
</html>
|
44
static/index.js
Normal file
44
static/index.js
Normal file
@@ -0,0 +1,44 @@
|
||||
const loadItems = () => {
|
||||
$.ajax(
|
||||
'product',
|
||||
{
|
||||
success: data => {
|
||||
const boxes = data.map(product =>
|
||||
`<div class="product">
|
||||
<input type="checkbox" class="delete-checkbox" value="${product.id}">
|
||||
<p>
|
||||
${product.sku}<br>
|
||||
${product.name}<br>
|
||||
${product.price} $<br>
|
||||
${product.attribute}
|
||||
</p>
|
||||
</div>`
|
||||
);
|
||||
|
||||
$('#products').html(boxes.join('\n'));
|
||||
},
|
||||
error: jqXHR => alert(jqXHR.responseText),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
loadItems();
|
||||
|
||||
$('#delete-product-btn').on('click', () => {
|
||||
let values = [];
|
||||
const checkboxes = document.querySelectorAll('input[class="delete-checkbox"]:checked');
|
||||
checkboxes.forEach(checkbox => values.push(checkbox.value));
|
||||
|
||||
if(values.length) {
|
||||
$.ajax(
|
||||
`product?id=${values.join(',')}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
success: loadItems,
|
||||
error: jqXHR => alert(jqXHR.responseText),
|
||||
}
|
||||
)
|
||||
} else {
|
||||
alert('Please select a product.');
|
||||
}
|
||||
});
|
@@ -14,16 +14,15 @@ body {
|
||||
margin: 0 1em;
|
||||
}
|
||||
|
||||
#buttons a {
|
||||
.header-button {
|
||||
align-self: center;
|
||||
margin: 0 2em;
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
font-family: cursive;
|
||||
padding: .5em 1em;
|
||||
font-size: 1rem;
|
||||
margin: 0 2em;
|
||||
align-self: center;
|
||||
background-color: white;
|
||||
border: 2px solid black;
|
||||
box-shadow: 2px 2px black;
|
||||
@@ -56,3 +55,23 @@ body {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#main {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
td:nth-child(2) {
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
#productType {
|
||||
margin-left: 2em;
|
||||
}
|
Reference in New Issue
Block a user