changed dir structure to handle second version of the book

This commit is contained in:
2022-11-25 11:46:00 +01:00
parent b6fd7876d3
commit 5f1f5e847d
35 changed files with 7 additions and 0 deletions

View File

@@ -0,0 +1,161 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Service\Router;
use App\Service\Template;
use App\Repository\UserRepository;
class AddressBookAdminController
{
public function __construct(
private readonly Template $template,
private readonly User $user,
private readonly UserRepository $userRepository,
private readonly Router $router
)
{
}
private function adminCheck(): void
{
if (!$this->user->isAdmin()) {
$this->template->render(templateName: 'status/403.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
}
public function admin(): never
{
$this->adminCheck();
$this->template->render(templateName: 'admin/index.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function adminUser(): never
{
$this->adminCheck();
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
}
public function adminUserEdit(array $parameters): never
{
$this->adminCheck();
if (!empty($_POST)) {
if (!empty($_POST['is_admin'])) {
$isAdmin = 1;
} else {
$isAdmin = 0;
}
if (empty($_POST['new_password'])) {
$current = $this->userRepository->findByID(id: $_POST['id']);
$password = $current->getPassword();
$updateUser = new User(nick: $_POST['nick'], password: $password, first: $_POST['first'], last: $_POST['last'], id: $_POST['id'], isAdmin: $isAdmin);
} else {
$password = $_POST['new_password'];
$updateUser = new User(nick: $_POST['nick'], newPassword: $password, first: $_POST['first'], last: $_POST['last'], id: $_POST['id'], isAdmin: $isAdmin);
}
$this->userRepository->update(user: $updateUser);
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
}
$editUser = $this->userRepository->findByNick(nick: $parameters['nick']);
$this->template->render(templateName: 'admin/users_edit.html.php', vars: [
'user' => $this->user,
'editUser' => $editUser,
'router' => $this->router
]);
}
public function adminUserAdd(): never
{
$this->adminCheck();
$nick = $_POST['nick'];
if ($this->userRepository->findByNick(nick: $nick)) {
die("User: $nick already exists");
}
if (!empty($_POST)) {
$isAdmin = empty($_POST['is_admin']) ? 0 : 1;
$user = new User(nick: $_POST['nick'], newPassword: $_POST['new_password'], first: $_POST['first'], last: $_POST['last'], isAdmin: $isAdmin);
if ($this->userRepository->insert(user: $user)) {
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
} else {
die("Error inserting user");
}
}
$this->template->render(templateName: 'admin/users_add.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function adminUserDelete(array $parameters): never
{
$this->adminCheck();
$nick = $parameters['nick'];
if ($user = $this->userRepository->findByNick(nick: $nick)) {
if ($this->userRepository->delete(user: $user)) {
$users = $this->userRepository->findAll();
$this->template->render(templateName: 'admin/users.html.php', vars: [
'user' => $this->user,
'users' => $users,
'router' => $this->router
]);
} else {
die("Error deleting user");
}
} else {
$this->template->render(templateName: 'status/404.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
}
}

View File

@@ -0,0 +1,133 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Entity\AddressBookEntry;
use App\Enums\StatusCode;
use App\Enums\UserAuth;
use App\Service\Router;
use App\Service\Template;
use App\Repository\AddressRepository;
class AddressBookController
{
public function __construct(
private readonly Template $template,
private readonly User $user,
private readonly AddressRepository $addressRepository,
private readonly Router $router
)
{
// empty body
}
public function main(): never
{
if ($this->user->getAuth() != UserAuth::AUTH_ANONYMOUS) {
$addresses = $this->addressRepository->findAll();
}
$this->template->render(templateName: 'index.html.php', vars: [
'user' => $this->user,
'router' => $this->router,
'addresses' => $addresses ?? []
]);
}
public function addAddress(): never
{
if (!empty($_POST)) {
$address = new AddressBookEntry(owner: $_POST['owner'], first: $_POST['first'], last: $_POST['last'], street: $_POST['street'], zip: $_POST['zip'], city: $_POST['city'], phone: $_POST['phone']);
if ($this->addressRepository->insert(address: $address)) {
$addresses = $this->addressRepository->findAll();
$this->template->render(templateName: 'index.html.php', vars: [
'user' => $this->user,
'addresses' => $addresses,
'router' => $this->router
]);
} else {
die("Error inserting user");
}
}
$this->template->render(templateName: 'addressbook/add_address.html.php', vars: [
'user' => $this->user,
'router' => $this->router
]);
}
public function updateAddress(): void
{
$_POST = json_decode(json: file_get_contents(filename: "php://input"), associative: true);
if (empty($_POST)) {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
if ($address = new AddressBookEntry(owner: $_POST['owner'], first: $_POST['first'], last: $_POST['last'], street: $_POST['street'], zip: $_POST['zip'], city: $_POST['city'], phone: $_POST['phone'], id: $_POST['id'])) {
if ($this->addressRepository->update(address: $address)) {
$status = 200;
$message = 'OK';
} else {
$status = 400;
$message = 'BAD_REQUEST';
}
} else {
$status = 400;
$message = "BAD REQUEST";
}
$this->template->renderJson(results: [
'status' => $status,
'message' => $message
]);
}
public function deleteAddress(): void
{
$_POST = json_decode(json: file_get_contents(filename: "php://input"), associative: true);
if (empty($_POST)) {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
if ($address = $this->addressRepository->findByID(id: $_POST['id'])) {
if ($this->addressRepository->delete(addressBookEntry: $address)) {
$this->template->renderJson(results: [
'status' => 200,
'message' => 'OK'
]);
} else {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
} else {
$this->template->renderJson(results: [
'status' => 400,
'message' => 'BAD REQUEST'
]);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Controller;
use App\Entity\User;
use App\Repository\UserRepository;
use App\Service\Router;
use App\Service\Template;
class SecurityController
{
public function __construct(
private readonly Template $template,
private readonly UserRepository $userRepository,
private readonly Router $router
)
{
}
public function login(): never
{
if (!empty($_POST)) {
$nick = $_POST['nick'] ?? '';
$password = $_POST['password'] ?? '';
if ($nick && $password) {
$nick = strtolower(string: $nick);
if ($user = $this->userRepository->findbyNick(nick: $nick)) {
if (password_verify(password: $password, hash: $user->getPassword())) {
$_SESSION['user_id'] = $user->getId();
header(header: 'Location: /');
exit(0);
} else {
$message = "Wrong credentials.";
}
} else {
$message = "User not found.";
}
} else {
$message = 'You need to enter your credentials.';
}
}
$this->template->render(templateName: 'security/login.html.php', vars: [
'user' => $user ?? new User(),
'message' => $message ?? '',
'router' => $this->router
]);
}
function logout(): void
{
session_unset();
header(header: 'Location: /');
}
}

View File

@@ -0,0 +1,107 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
class AddressBookEntry
{
public function __construct(
private int $owner,
private string $first,
private string $last,
private string $street,
private string $zip,
private string $city,
private string $phone,
private int $id = 0,
)
{
// empty body
}
public function getOwner(): int
{
return $this->owner;
}
public function setOwner(int $owner): void
{
$this->owner = $owner;
}
public function getStreet(): string
{
return $this->street;
}
public function setStreet(string $street): void
{
$this->street = $street;
}
public function getZip(): string
{
return $this->zip;
}
public function setZip(string $zip): void
{
$this->zip = $zip;
}
public function getCity(): string
{
return $this->city;
}
public function setCity(string $city): void
{
$this->city = $city;
}
public function getPhone(): string
{
return $this->phone;
}
public function setPhone(string $phone): void
{
$this->phone = $phone;
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getFirst(): string
{
return $this->first;
}
public function setFirst(string $first): void
{
$this->first = $first;
}
public function getLast(): string
{
return $this->last;
}
public function setLast(string $last): void
{
$this->last = $last;
}
}

View File

@@ -0,0 +1,77 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
use Closure;
class Route
{
public function __construct(
private string $name,
private string $route,
private string $regEx,
private array $parameters,
private Closure $callback
)
{
// empty body
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): void
{
$this->name = $name;
}
public function getRoute(): string
{
return $this->route;
}
public function setRoute(string $route): void
{
$this->route = $route;
}
public function getRegEx(): string
{
return $this->regEx;
}
public function setRegEx(string $regEx): void
{
$this->regEx = $regEx;
}
public function getParameters(): array
{
return $this->parameters;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function getCallback(): Closure
{
return $this->callback;
}
public function setCallback(Closure $callback): void
{
$this->callback = $callback;
}
}

114
Vanilla/src/Entity/User.php Normal file
View File

@@ -0,0 +1,114 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Entity;
use App\Enums\UserAuth;
class User
{
public function __construct(
private string $nick = '',
private string $password = '',
private readonly string $newPassword = '',
private string $first = '',
private string $last = '',
private int $id = 0,
private bool $isAdmin = false,
private UserAuth $userAuth = UserAuth::AUTH_ANONYMOUS
)
{
if (!empty($this->newPassword)) {
echo "password";
$this->password = password_hash(password: $this->newPassword, algo: PASSWORD_ARGON2I);
}
if (session_status() === PHP_SESSION_ACTIVE) {
// ANONYMOUS has id 0
if ($this->id != 0) {
if ($this->isAdmin) {
$this->userAuth = UserAuth::AUTH_ADMIN;
} else {
$this->userAuth = UserAuth::AUTH_USER;
}
}
}
}
public function getNick(): string
{
return $this->nick;
}
public function setNick(string $nick): void
{
$this->nick = $nick;
}
public function getPassword(): string
{
return $this->password;
}
public function setPassword(string $password): void
{
$this->password = $password;
}
public function getFirst(): string
{
return $this->first;
}
public function setFirst(string $first): void
{
$this->first = $first;
}
public function getLast(): string
{
return $this->last;
}
public function setLast(string $last): void
{
$this->last = $last;
}
public function getId(): int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function isAdmin(): bool
{
return $this->isAdmin;
}
public function setIsAdmin(bool $isAdmin): void
{
$this->isAdmin = $isAdmin;
}
public function getAuth(): UserAuth
{
return $this->userAuth;
}
public function setAuth(UserAuth $userAuth): void
{
$this->userAuth = $userAuth;
}
}

View File

@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Enums;
enum UserAuth
{
case AUTH_ANONYMOUS;
case AUTH_USER;
case AUTH_ADMIN;
}

View File

@@ -0,0 +1,174 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Repository;
use App\Entity\AddressBookEntry;
use App\Service\DatabaseConnection;
use PDO;
use PDOException;
/**
* Handles CRUD of Addresses class.
*/
class AddressRepository
{
public function __construct(private readonly DatabaseConnection $databaseConnection)
{
// empty body
}
public function findAll(string $orderBy = 'last'): array
{
$sql = "
SELECT id, owner, first, last, street, zip, city, phone
FROM " . DatabaseConnection::TABLE_ADDRESSES . "
ORDER BY :order";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':order', var: $orderBy);
$statement->execute();
$addresses = [];
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$address = new AddressBookEntry(
owner: htmlspecialchars(string: $result['owner']),
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
street: htmlspecialchars(string: $result['street']),
zip: htmlspecialchars(string: $result['zip']),
city: htmlspecialchars(string: $result['city']),
phone: htmlspecialchars(string: $result['phone']),
id: htmlspecialchars(string: $result['id']));
$addresses[] = $address;
}
return $addresses;
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByID(int $id): ?AddressBookEntry
{
$sql = "
SELECT id, owner, first, last, street, zip, city, phone
FROM " . DatabaseConnection::TABLE_ADDRESSES . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':id', var: $id);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new AddressBookEntry(
owner: htmlspecialchars(string: $result['owner']),
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
street: htmlspecialchars(string: $result['street']),
zip: htmlspecialchars(string: $result['zip']),
city: htmlspecialchars(string: $result['city']),
phone: htmlspecialchars(string: $result['phone']),
id: htmlspecialchars(string: $result['id']));
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function insert(AddressBookEntry $address): bool|string
{
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_ADDRESSES . " (owner, first, last, city, zip, street, phone)
VALUES (:owner, :first, :last, :city, :zip, :street, :phone)";
try {
$owner = $address->getOwner();
$first = $address->getFirst();
$last = $address->getLast();
$city = $address->getCity();
$zip = $address->getZip();
$street = $address->getStreet();
$phone = $address->getPhone();
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':owner', var: $owner);
$statement->bindParam(param: ':first', var: $first);
$statement->bindParam(param: ':last', var: $last);
$statement->bindParam(param: ':city', var: $city);
$statement->bindParam(param: ':zip', var: $zip);
$statement->bindParam(param: ':street', var: $street);
$statement->bindParam(param: ':phone', var: $phone);
$statement->execute();
return $this->databaseConnection->getConnection()->lastInsertId();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function update(AddressBookEntry $address): bool|int
{
$id = $address->getId();
$first = $address->getFirst();
$last = $address->getLast();
$street = $address->getStreet();
$zip = $address->getZip();
$city = $address->getCity();
$phone = $address->getPhone();
$sql = "
UPDATE " . DatabaseConnection::TABLE_ADDRESSES . " SET
first = :first,
last = :last,
street = :street,
zip = :zip,
city = :city,
phone = :phone
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: 'id', var: $id);
$statement->bindParam(param: 'first', var: $first);
$statement->bindParam(param: 'last', var: $last);
$statement->bindParam(param: 'street', var: $street);
$statement->bindParam(param: 'zip', var: $zip);
$statement->bindParam(param: 'city', var: $city);
$statement->bindParam(param: 'phone', var: $phone);
return $statement->execute();
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
}
public function delete(AddressBookEntry $addressBookEntry): int
{
$sql = "
DELETE FROM " . DatabaseConnection::TABLE_ADDRESSES . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$id = $addressBookEntry->getId();
$statement->bindParam(param: 'id', var: $id);
return $statement->execute();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
}

View File

@@ -0,0 +1,196 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Repository;
use App\Service\DatabaseConnection;
use App\Entity\User;
use PDO;
use PDOException;
/**
* Handles CRUD of User class.
*/
class UserRepository
{
public function __construct(private readonly DatabaseConnection $databaseConnection)
{
// empty body
}
public function findAll(string $orderBy = 'nick'): array
{
$users = [];
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
ORDER BY :order";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':order', var: $orderBy);
$statement->execute();
while ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
$user = new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
$users[] = $user;
}
return $users;
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByID(int $id): ?User
{
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':id', var: $id);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function findByNick(string $nick): ?User
{
$nick = strtolower(string: $nick);
$sql = "
SELECT id, nick, password, first, last, is_admin
FROM " . DatabaseConnection::TABLE_USERS . "
WHERE nick = :nick";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':nick', var: $nick);
$statement->execute();
if ($result = $statement->fetch(mode: PDO::FETCH_ASSOC)) {
return new User(
nick: htmlspecialchars(string: $result['nick']),
password: $result['password'],
first: htmlspecialchars(string: $result['first']),
last: htmlspecialchars(string: $result['last']),
id: $result['id'],
isAdmin: $result['is_admin']);
} else {
return null;
}
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function insert(User $user): bool|string
{
$sql = "
INSERT INTO " . DatabaseConnection::TABLE_USERS . " (nick, password, first, last, is_admin)
VALUES (:nick, :password, :first, :last, :is_admin)";
try {
$nick = $user->getNick();
$password = $user->getPassword();
$first = $user->getFirst();
$last = $user->getLast();
$isAdmin = $user->isAdmin() ? 1 : 0;
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: ':nick', var: $nick);
$statement->bindParam(param: ':password', var: $password);
$statement->bindParam(param: ':first', var: $first);
$statement->bindParam(param: ':last', var: $last);
$statement->bindParam(param: ':is_admin', var: $isAdmin);
$statement->execute();
return $this->databaseConnection->getConnection()->lastInsertId();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
public function update(User $user): bool
{
$id = $user->getId();
$nick = $user->getNick();
$first = $user->getFirst();
$last = $user->getLast();
$isAdmin = $user->isAdmin() ? 1 : 0;
if ($user->getPassword()) {
$password = $user->getPassword();
} else {
$current = $this->findByID(id: $id);
$password = $current->getPassword();
}
$sql = "
UPDATE " . DatabaseConnection::TABLE_USERS . " SET
nick = :nick,
password = :password,
first = :first,
last = :last,
is_admin = :is_admin
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$statement->bindParam(param: 'id', var: $id);
$statement->bindParam(param: 'nick', var: $nick);
$statement->bindParam(param: 'password', var: $password);
$statement->bindParam(param: 'first', var: $first);
$statement->bindParam(param: 'last', var: $last);
$statement->bindParam(param: 'is_admin', var: $isAdmin);
return $statement->execute();
} catch (PDOException $e) {
echo $e->getMessage();
return false;
}
}
public function delete(User $user): bool
{
$sql = "
DELETE FROM " . DatabaseConnection::TABLE_USERS . "
WHERE id = :id";
try {
$statement = $this->databaseConnection->getConnection()->prepare(query: $sql);
$id = $user->getId();
$statement->bindParam(param: 'id', var: $id);
return $statement->execute();
} catch (PDOException $e) {
exit($e->getMessage());
}
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
/**
*
*/
class Config
{
private array $config;
public function __construct()
{
// Check for either config.json.local or config.json.
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json.local";
if (!file_exists(filename: $configFile)) {
$configFile = dirname(path: __DIR__, levels: 2) . "/config.json";
}
if (!file_exists(filename: $configFile)) {
die('Missing config file');
}
$configJSON = file_get_contents(filename: $configFile);
if (json_decode(json: $configJSON) === null) {
die('Config file is not valid JSON.');
}
$this->config = json_decode(json: $configJSON, associative: true);
}
public function getConfig(string $configKey): string
{
return $this->config[$configKey];
}
}

View File

@@ -0,0 +1,68 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use App\Controller\AddressBookAdminController;
use App\Controller\AddressBookController;
use App\Controller\SecurityController;
use App\Entity\User;
use App\Repository\AddressRepository;
use App\Repository\UserRepository;
/*
* A quick and dirty class container for DI.
* Caveat: Classes are always instantiated
* No autowiring (yet, maybe later, but it might fit for a demo)
*/
class Container
{
private AddressBookController $addressBook;
private AddressBookAdminController $addressBookAdmin;
private AddressRepository $addressRepository;
private Config $config;
private DatabaseConnection $databaseConnection;
private Router $router;
private SecurityController $securityController;
private Template $template;
private User $user;
private UserRepository $userRepository;
public function __construct()
{
$this->config = new Config();
$this->databaseConnection = new DatabaseConnection(config: $this->config);
$this->template = new Template(templateDir: dirname(path: __DIR__, levels: 2) . '/templates/');
$this->router = new Router(template: $this->template);
$this->userRepository = new UserRepository(databaseConnection: $this->databaseConnection);
$this->addressRepository = new AddressRepository(databaseConnection: $this->databaseConnection);
$this->securityController = new SecurityController(template: $this->template, userRepository: $this->userRepository, router: $this->router);
if (empty($_SESSION['user_id'])) {
$this->user = new User(); // ANONYMOUS
} else {
$this->user = $this->userRepository->findByID(id: $_SESSION['user_id']);
}
$this->addressBook = new AddressBookController(template: $this->template, user: $this->user, addressRepository: $this->addressRepository, router: $this->router);
$this->addressBookAdmin = new AddressBookAdminController(template: $this->template, user: $this->user, userRepository: $this->userRepository, router: $this->router);
}
public function get(string $className): object
{
return match ($className) {
'App\Controller\AddressBookController' => $this->addressBook,
'App\Controller\AddressBookAdminController' => $this->addressBookAdmin,
'App\Controller\SecurityController' => $this->securityController,
'App\Service\Router' => $this->router,
//default => throw new Exception(message: "Missing class definition: $class")
default => die("Missing class definition: $className")
};
}
}

View File

@@ -0,0 +1,46 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use PDO;
/**
* Take care of the PDO object.
*/
class DatabaseConnection
{
private PDO $dbConnection;
// Currently no prefixes are used, but could be easily added to config.json.
const TABLE_PREFIX = '';
const TABLE_USERS = self::TABLE_PREFIX . "users";
const TABLE_ADDRESSES = self::TABLE_PREFIX . "addresses";
public function __construct(private readonly Config $config)
{
$dbHost = $this->config->getConfig(configKey: 'dbHost');
$dbPort = $this->config->getConfig(configKey: 'dbPort');
$dbDatabase = $this->config->getConfig(configKey: 'dbDatabase');
$dbUser = $this->config->getConfig(configKey: 'dbUser');
$dbPassword = $this->config->getConfig(configKey: 'dbPassword');
$this->dbConnection = new PDO(
dsn: "mysql:host=$dbHost;port=$dbPort;charset=utf8mb4;dbname=$dbDatabase",
username: $dbUser,
password: $dbPassword
);
$this->dbConnection->setAttribute(attribute: PDO::ATTR_ERRMODE, value: PDO::ERRMODE_EXCEPTION);
}
public function getConnection(): PDO
{
return $this->dbConnection;
}
}

View File

@@ -0,0 +1,124 @@
<?php
/*
* Copyright (c) 2022. Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
*/
namespace App\Service;
use App\Entity\Route;
use Closure;
/*
* A small router implementation for the address book demo.
* Currently it doesn't handle GET requests, as not needed.
* But if I reuse the code in my bindApi I'll maybe support GET as well.
*/
class Router
{
/*
* The easiest way to differentiate between static and dynamic routes is using
* two arrays, no need to pollute the class Route with that information
*/
private array $staticRoutes = [];
private array $dynamicRoutes = [];
public function __construct(private readonly Template $template)
{
// empty body
}
/*
* This method takes a route like /admin/users/{user} and creates a regex to match on call
* More complex routes like /posts/{thread}/show/{page} are supported as well.
*/
function addRoute(string $name, string $route, Closure $callback): void
{
// check for parameters
preg_match_all(pattern: "/(?<={).+?(?=})/", subject: $route, matches: $matches);
$parameters = $matches[0];
// create regex for route:
$regex = preg_replace(pattern: '/{.+?}/', replacement: '([a-zA-Z0-9]*)', subject: $route);
// escape \ in regex
$regex = '/^' . str_replace(search: "/", replace: '\\/', subject: $regex) . '$/i';
$route = new Route(name: $name, route: $route, regEx: $regex, parameters: $parameters, callback: $callback);
if ($parameters) {
$this->dynamicRoutes[] = $route;
} else {
$this->staticRoutes[] = $route;
}
}
/*
* Checks if there is a known route and executes the callback.
*/
public function handleRouting(): void
{
$requestUri = $_SERVER['REQUEST_URI'];
/*
* Static routes have precedence over dynamic ones, so
* /admin/user/add to add and
* /admin/user/{name} to edit is possible.
* A user named "add" of course not :)
*
* But who wants to call their users "add" or "delete"?
* That's as weird as Little Bobby Tables … (https://xkcd.com/327/)
*/
foreach ($this->staticRoutes as $route) {
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
call_user_func(callback: $route->getCallback());
// We've found our route, bail out.
return;
}
}
foreach ($this->dynamicRoutes as $route) {
// PHPStorm doesn't know that $parameters are always available,
// (as these are dynamic routes) so I init the array just to make PHPStorm happy.
$parameters = [];
if (preg_match(pattern: $route->getRegex(), subject: $requestUri, matches: $matches)) {
foreach ($route->getParameters() as $id => $parameter) {
$parameters[$parameter] = $matches[$id + 1];
}
// PHP is mad about named parameters in call_user_func when adding parameters.
// Uncaught Error: Unknown named parameter $args in <sourceFile>
// But PHPStorm seems happy without them. So what?
call_user_func($route->getCallback(), $parameters);
return;
}
}
// if no route is matched, throw a 404
$this->template->render(templateName: 'status/404.html.php');
}
public function path(string $routeName, array $vars = [])
{
foreach (array_merge($this->dynamicRoutes, $this->staticRoutes) as $route) {
if ($route->getName() == $routeName) {
if ($vars) {
// build route for dynamic routes
$route = $route->getRoute();
// replace placeholder with current values
foreach ($vars as $key => $value) {
$route = str_replace(search: '{' . $key . '}', replace: $value, subject: $route);
}
return $route;
} else {
return $route->getRoute();
}
}
}
// no 404, this is reached only if the code is buggy
die("Missing Route: $routeName");
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* Copyright (c) 2022 Micha Espey <tracer@24unix.net>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Service;
/*
* As I'm not allowed to use 3rd party code like Twig or Smarty I ended up
* using PHP as a templating engine.
*/
class Template
{
/*
* Just store the information about the template base dir.
*/
public function __construct(private readonly string $templateDir)
{
// empty body
}
/*
* Add variables to template and throw it out
*/
public function render(string $templateName, array $vars = []): never
{
// assign template vars
foreach ($vars as $name => $value) {
$$name = $value;
}
$templateFile = $this->templateDir . $templateName;
if (file_exists(filename: $templateFile)) {
include $this->templateDir . $templateName;
} else {
die("Missing template: $templateFile");
}
exit(0);
}
/*
* For AJAX calls, return json
*/
public function renderJson(array $results): never
{
http_response_code(response_code: $results['status']);
echo json_encode(value: $results);
exit(0);
}
}

18
Vanilla/src/bootstrap.php Normal file
View File

@@ -0,0 +1,18 @@
<?php
spl_autoload_register(callback: function ($className) {
$prefix = 'App';
$baseDir = __DIR__;
$prefixLen = strlen(string: $prefix);
if (strncmp(string1: $prefix, string2: $className, length: $prefixLen) !== 0) {
die("Invalid class: $className");
}
$realClassNamePSRpath = substr(string: $className, offset: $prefixLen);
$classLocation = $baseDir . str_replace(search: '\\', replace: '/', subject: $realClassNamePSRpath) . '.php';
if (file_exists(filename: $classLocation)) {
require $classLocation;
} else {
die("Invalid class: $className");
}
});