2021
Apparel (PHP)

Apparel DB - Erstes PHP Projekt

Datum: Dezember 2021
Lesedauer: 6 Minuten


home

Beschreibung

Meine Aufgabe war es, mit PHP eine CRUD-Webapplikation zu erstellen. In diesem Post beschreibe ich mein Vorgehen und zeige den aktuellen Projektstand.

Ideenfindung

Da ich mich für Caps und insbesondere Sneakers interessiere, dachte ich an eine Art Produktverwaltung.
In dieser kann man dann Artikel hinzufügen, editieren und löschen. Zudem soll es eine Übersichtsseite geben, welche einen Überblick über die persönlichen Produkte bietet.

Planung

Nachdem ich mich für die Idee entschieden habe, begann ich mögliche Klassen und deren Zusammenspiel zu definieren. Dies tat ich, indem ich ein UML-Diagramm erstellte.

uml

Setup

Als das Diagramm fertig war, konnte ich mit dem Setup beginnen. Dazu musste ich drei Dinge vorbereiten.

MAMP

Damit man PHP verwenden kann, muss die Skriptsprache installiert oder von einem Webserver zur Verfügung gestellt werden. Ich verwende MAMP (opens in a new tab) als Webserver.
Da ich das Projekt im htdocs Ordner anlegte, kann ich es über den Localhost erreichen.

Datenbank

Als Datenbank verwende ich MySQL. Die Daten kann ich über phpMyAdmin verwalten.
Diese Seite ist ebenfalls über den Localhost zugänglich: http://localhost:8888/phpMyAdmin

Darin habe ich dann die entsprechenden Tabellen erstellt.

db

IDE

Als Entwicklungsumgebung verwende ich PhpStorm. Alternativ könnten Eclipse, NetBeans oder Visual Studio Code genutzt werden.

Software

Mein Projekt ist folgendermassen strukturiert.

struktur

Root

Im Root Verzeichnis trifft man hauptsächlich die Seiten der Webseite an. Die einzelnen Files werden hier genauer beschrieben.

index.php

Diese Datei enthält die Startseite. Auf dieser hat man die Wahl, ob man sich die Caps oder Sneakers ansehen will.

header.php

Der Header wird in allen Seiten verwendet und enthält das Grundlayout:

header.php
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title><?= $title; ?></title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
<nav>
    <ul>
        <li><a href="index.php">Home</a></li>
        <li><a href="editProduct.php">Edit</a></li>
    </ul>
</nav>

Will man nun den Header verwenden, so muss man ihn nur inkludieren. Hat man zuvor der Titel Variable noch einen Wert zugewiesen, so wird dieser dann angezeigt.

editProduct.php
$title = "Apparel DB | Edit Product";
include "header.php";

sneakers.php

Diese Datei verwendet Database.php Objekt, um die Daten von der Datenbank zu holen. Diese stellt sie dann dar, indem sie über die entsprechenden Arrays loopt.

foreach in sneakers.php
<?php foreach ($sneakers as $sneaker): ?>
    <div class='m-product'>
        <img src="<?= $sneaker->getImg(); ?>" alt="sneaker">
        <?php if ($sneaker->getName() !== null) : ?>
            <p>
                <span class="a-title"><?= $sneaker->getName(); ?></span>
                <br>
        <?php endif; ?>
 
        <?php if (!empty($sneaker->getBrands())) : ?>
            <?php foreach ($sneaker->getBrands() as $brand): ?>
                <?= $brand->getName(); ?>.
            <?php endforeach; ?>
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getModel() !== null) : ?>
            <?= $sneaker->getModel(); ?>
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getColour() !== null) : ?>
            Color: <?= $sneaker->getColour(); ?>
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getQuantity() !== null) : ?>
            Quantity: <?= $sneaker->getQuantity(); ?>
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getSize() !== null) : ?>
            Size: <?= $sneaker->getSize(); ?>
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getPrice() !== null) : ?>
            Price: <?= $sneaker->getPrice(); ?> $
            <br>
        <?php endif; ?>
 
        <?php if ($sneaker->getRetail() !== null) : ?>
            Retail: <?= $sneaker->getRetail(); ?> $
            <br>
        <?php endif; ?>
 
        <?php if (!empty($sneaker->getArtists())) : ?>
            Artist/s:
            <?php foreach ($sneaker->getArtists() as $artist): ?>
                <?= $artist->getName(); ?>
                <br>
            <?php endforeach; ?>
        <?php endif; ?>
 
        <?php if (!empty($sneaker->getDesigners())) : ?>
            Designer/s:
            <?php foreach ($sneaker->getDesigners() as $designer): ?>
                <?= $designer->getName(); ?>
                <br>
            <?php endforeach; ?>
        <?php endif; ?>
 
        <?php if (!empty($sneaker->getAthletes())) : ?>
            Athlete/s:
            <?php foreach ($sneaker->getAthletes() as $athlete): ?>
                <?= $athlete->getName(); ?>
                <br>
            <?php endforeach; ?>
        <?php endif; ?>
 
        </p>
    </div>
<?php endforeach; ?>

editProduct.php

Um zu editieren werden ebenfalls die Daten mithilfe der Database Klasse geholt und dann in Input Felder geschrieben.
Hat der User etwas editiert, erstellt oder gelöscht, so wird das Formular gesendet. Hierbei wird der Post ans eigene File geschickt. Deshalb enthält dieses File auch Logik.

Es kann die getätigten Änderungen erkennen, verarbeiten, zur Database.php Datei schicken und anschliessend eine Message ausgeben.

So wird beispielsweise eine neue Brand erstellt.

add brand in editProduct.php
if ($post->get("addBrand") !== null) {
    if (!empty($post->get("brandNewName"))) {
        $brand = new Brand(0, $post->get("brandNewName"));
        $data->addBrand($brand);
        $messageHandler->addSuccessMessage("successfully added " . $brand->getName());
    }
}

Chosen

Bei manchen Feldern wird erwartet, dass eine bestimmte Anzahl an definierter Werte ausgewählt werden können. Dies ist z. B bei den Brands der Fall:

chosen-brands

Um ein solches multiple select Feld darzustellen, verwendete ich CHOSEN. Dies ist ein jQuery Plugin. Importiert man es ins Projekt, so muss man nur die CSS Klasse definieren, welche die Styles erhalten soll.

Scripts

Die Datenbank habe ich manuell mit Daten gefüllt. Wenn ich nun während dem Testen etwas ändere oder sogar zerstöre, dann würde dies viel Zeit kosten, alles wieder einzutragen.
Aus diesem Grund habe ich im Scripts Ordner zwei Dateien angelegt.

createDb.php

Erstellt die Datenbank falls nicht vorhanden:

createDb.php
<?php
$mysqli = new mysqli("localhost", "root", "root");
if (!mysqli_select_db($mysqli, "apparel")) {
    $mysqli->query("CREATE DATABASE apparel");
    mysqli_select_db($mysqli, "apparel");
}
$mysqli->close();
?>

createTables.php

Erstellt die Tabellen, z. B. für brand:

<?php filename="brand tale create in createTables.php"
$mysqli = new mysqli("localhost", "root", "root", "apparel");
$brandSql = "
 CREATE TABLE IF NOT EXISTS brand(
    name VARCHAR(25) NOT NULL ,
    brand_id INT NOT NULL AUTO_INCREMENT ,
    PRIMARY KEY (brand_id)
);
";
$mysqli->query($brandSql);
?>

Klassenstruktur

Database.php

Diese Klasse ist das Herzstück der Applikation. Mit den connect und disconnect Methoden ist sie für die Datenbank Anbindung zuständig.
Zudem werden auch die CRUD Operationen auf der Datenbank über ein Objekt dieser ORM Klasse gesteuert.

public function delete(Entity $entity) {
    $this->mysqli->query("DELETE FROM " . $entity->getTableName() . "
        WHERE " . $entity->getTablePrimaryKey() . " = " . $entity->getId());
}

Entity-Klassen

Im Entity Ordner befinden sich die Klassen, welche die Produkte abbilden.

entity

Neben der Brand, Sport und Team Klasse, befindet sich die Entity Klasse. Diese Klasse ist abstrakt. Dies bedeutet, dass aus ihr kein Objekt instanziiert werden kann.

Diese Klasse enthält die Attribute Name und Id. Sie wird von allen anderen Klassen vererbt.
Zudem hat Entity.php zwei abstrakte Methoden definiert. Diese haben noch keine Funktionalität und müssen von den jeweiligen erbenden Klassen implementiert werden.

abstrakte Methoden in Entity.php
abstract public function getTableName();
abstract public function getTablePrimaryKey();

Brand erbt von Entity und ist daher verpflichtet, die zwei Methoden zu implementieren.

Beispiel: Brand-Klasse
Brand.php
class Brand extends Entity {
    public function getTableName() {
        return "brand";
    }
    public function getTablePrimaryKey() {
        return "brand_id";
    }
}

Ohne diese Methoden, müsste Database.php über mehrere delete Funktionen verfügen.

  • deleteBrand(Brand $brand)
  • deleteSneaker(Sneaker $sneaker)
  • ...

Mithilfe der Entity Klasse konnte man das vereinheitlichen.

Nun reicht eine Methode, um die korrekten SQL Statements durchzuführen. Dieser Methode können dann alle Objekte, welche von Entity erben, mitgegeben werden.
Dadurch kann nicht nur die Id, sondern auch der Tabellenname und Primärschlüssel ausgelesen werden.

delete in Database.php
public function delete(Entity $entity) {
	$this->mysqli->query("DELETE FROM " . $entity->getTableName() . "
		WHERE
		" . $entity->getTablePrimaryKey() . " = " . $entity->getId());
}

Person

In diesem Verzeichnis befinden sich die Klassen, welche Personen abbilden.

Person.php ist eine abstrakte Klasse. Sie erbt ebenfalls von Entity.php und speichert zusätzliche Attribute, welche alle Personen gemeinsam haben. Dazu gehört das Alter, der Wohnort und die Nationalität.
So müssen die Artist, Athlete und Designer Klassen nur noch Einzelheiten speichern.

Product

Im Product Ordner sind die zwei Produkt Klassen Cap und Sneaker. Auch hier gibt es wieder eine abstrakte Klasse, welche gemeinsame Attribute enthält.

MessageHandler

Um die Usability zu verbessern, habe ich einen MessageHandler eingebaut. Dieser zeigt, ob der SQL Befehl korrekt ausgeführt wurde.

Message.php

Die Message kann eine Nachricht und dessen Status speichern.

MessageHandler.php

Der Message Handler erstellt, speichert und gibt dann diese Nachrichten zurück.

Utility

Durch die Post Methode kann schädliche Befehle ins Programm gelangen. Deshalb ist es wichtig, dass man den Input validiert.
Um dies zu tun, habe ich eine Post Klasse geschrieben. Diese entfernt gefährliche Teile der Eingaben und gibt einen sauberen Wert zurück.

Post.php
namespace Utility;
 
class Post {
	private function clean(string $string) {
		return trim(htmlspecialchars(strip_tags($string)));
	}
 
	public function get(string $post) {
		if (isset($_POST[$post])) {
			if (is_array($_POST[$post])) {
				$postArray = [];
				foreach ($_POST[$post] as $item) {
					array_push($postArray, $this->clean($item));
				}
				return $postArray;
			} else {
				return $this->clean($_POST[$post]);
			}
		}
		return null;
	}
}

Auf der Zeile 10 wird überprüft, ob es sich beim Inhalt des Posts um einen Array handelt. In diesem Fall muss man über den Array loopen und nur die Werte formatieren. Ansonsten würde man den Array zerstören.

Fazit

Bis jetzt fand ich es eine sehr spannende Aufgabe. Gerade im Bereich Refactoring habe ich mich sehr verbessert. Zudem habe ich mehr über Namespaces, ORM und den Umgang mit Files gelernt. Auch das Zusammenspiel mit einer solchen Datenbank war mir bis anhin noch nicht so bekannt.