Set up modern PHP MVC project structure for GetYourBand platform
- Implemented clean MVC architecture with Router, Controller, and Model base classes - Created database migrations for users, bands, bookings, reviews, and availability - Set up Tailwind CSS with yellow color scheme and modern design - Added Alpine.js for reactive JavaScript components - Configured Vite for asset building and hot module replacement - Created authentication and role-based middleware - Implemented helper functions and configuration system - Added comprehensive README with setup instructions - Configured Apache with proper rewrite rules and security headers - Set up Composer and npm package management with modern dependencies
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Controller
|
||||
{
|
||||
protected function view(string $view, array $data = []): void
|
||||
{
|
||||
extract($data);
|
||||
|
||||
$viewPath = __DIR__ . '/../Views/' . str_replace('.', '/', $view) . '.php';
|
||||
|
||||
if (!file_exists($viewPath)) {
|
||||
throw new \RuntimeException("View not found: {$view}");
|
||||
}
|
||||
|
||||
require_once $viewPath;
|
||||
}
|
||||
|
||||
protected function json($data, int $status = 200): void
|
||||
{
|
||||
http_response_code($status);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data);
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function redirect(string $path): void
|
||||
{
|
||||
header("Location: {$path}");
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function back(): void
|
||||
{
|
||||
$referer = $_SERVER['HTTP_REFERER'] ?? '/';
|
||||
$this->redirect($referer);
|
||||
}
|
||||
|
||||
protected function input(string $key, $default = null)
|
||||
{
|
||||
return $_POST[$key] ?? $_GET[$key] ?? $default;
|
||||
}
|
||||
|
||||
protected function validate(array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
$data = [];
|
||||
|
||||
foreach ($rules as $field => $fieldRules) {
|
||||
$value = $this->input($field);
|
||||
$fieldRules = explode('|', $fieldRules);
|
||||
|
||||
foreach ($fieldRules as $rule) {
|
||||
if ($rule === 'required' && empty($value)) {
|
||||
$errors[$field][] = ucfirst($field) . ' is required';
|
||||
}
|
||||
|
||||
if (str_starts_with($rule, 'min:')) {
|
||||
$min = (int) substr($rule, 4);
|
||||
if (strlen($value) < $min) {
|
||||
$errors[$field][] = ucfirst($field) . " must be at least {$min} characters";
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($rule, 'max:')) {
|
||||
$max = (int) substr($rule, 4);
|
||||
if (strlen($value) > $max) {
|
||||
$errors[$field][] = ucfirst($field) . " must not exceed {$max} characters";
|
||||
}
|
||||
}
|
||||
|
||||
if ($rule === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field][] = ucfirst($field) . ' must be a valid email';
|
||||
}
|
||||
}
|
||||
|
||||
$data[$field] = $value;
|
||||
}
|
||||
|
||||
if (!empty($errors)) {
|
||||
$_SESSION['errors'] = $errors;
|
||||
$_SESSION['old'] = $data;
|
||||
$this->back();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
protected function auth()
|
||||
{
|
||||
return $_SESSION['user'] ?? null;
|
||||
}
|
||||
|
||||
protected function isAuthenticated(): bool
|
||||
{
|
||||
return isset($_SESSION['user']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Database\Database;
|
||||
use PDO;
|
||||
|
||||
abstract class Model
|
||||
{
|
||||
protected PDO $db;
|
||||
protected string $table;
|
||||
protected string $primaryKey = 'id';
|
||||
protected array $fillable = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = Database::connect();
|
||||
}
|
||||
|
||||
public function all(): array
|
||||
{
|
||||
$stmt = $this->db->query("SELECT * FROM {$this->table}");
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function find(int $id): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$this->primaryKey} = ? LIMIT 1");
|
||||
$stmt->execute([$id]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public function where(string $column, $value): array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = ?");
|
||||
$stmt->execute([$value]);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function first(string $column, $value): ?array
|
||||
{
|
||||
$stmt = $this->db->prepare("SELECT * FROM {$this->table} WHERE {$column} = ? LIMIT 1");
|
||||
$stmt->execute([$value]);
|
||||
$result = $stmt->fetch();
|
||||
return $result ?: null;
|
||||
}
|
||||
|
||||
public function create(array $data): int
|
||||
{
|
||||
$data = $this->filterFillable($data);
|
||||
$columns = implode(', ', array_keys($data));
|
||||
$placeholders = implode(', ', array_fill(0, count($data), '?'));
|
||||
|
||||
$sql = "INSERT INTO {$this->table} ({$columns}) VALUES ({$placeholders})";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute(array_values($data));
|
||||
|
||||
return (int) $this->db->lastInsertId();
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$data = $this->filterFillable($data);
|
||||
$set = implode(' = ?, ', array_keys($data)) . ' = ?';
|
||||
|
||||
$sql = "UPDATE {$this->table} SET {$set} WHERE {$this->primaryKey} = ?";
|
||||
$stmt = $this->db->prepare($sql);
|
||||
|
||||
return $stmt->execute([...array_values($data), $id]);
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$stmt = $this->db->prepare("DELETE FROM {$this->table} WHERE {$this->primaryKey} = ?");
|
||||
return $stmt->execute([$id]);
|
||||
}
|
||||
|
||||
public function query(string $sql, array $params = []): array
|
||||
{
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
return $stmt->fetchAll();
|
||||
}
|
||||
|
||||
public function execute(string $sql, array $params = []): bool
|
||||
{
|
||||
$stmt = $this->db->prepare($sql);
|
||||
return $stmt->execute($params);
|
||||
}
|
||||
|
||||
protected function filterFillable(array $data): array
|
||||
{
|
||||
if (empty($this->fillable)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return array_intersect_key($data, array_flip($this->fillable));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $middlewareStack = [];
|
||||
|
||||
public function get(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, $handler): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
public function group(array $attributes, callable $callback): void
|
||||
{
|
||||
$previousMiddleware = $this->middlewareStack;
|
||||
|
||||
if (isset($attributes['middleware'])) {
|
||||
$this->middlewareStack = array_merge(
|
||||
$this->middlewareStack,
|
||||
(array) $attributes['middleware']
|
||||
);
|
||||
}
|
||||
|
||||
$callback($this);
|
||||
|
||||
$this->middlewareStack = $previousMiddleware;
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, $handler): void
|
||||
{
|
||||
$this->routes[] = [
|
||||
'method' => $method,
|
||||
'path' => $path,
|
||||
'handler' => $handler,
|
||||
'middleware' => $this->middlewareStack,
|
||||
];
|
||||
}
|
||||
|
||||
public function dispatch(string $requestMethod, string $requestUri): void
|
||||
{
|
||||
$requestUri = parse_url($requestUri, PHP_URL_PATH);
|
||||
|
||||
foreach ($this->routes as $route) {
|
||||
if ($route['method'] !== $requestMethod) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pattern = $this->convertToPattern($route['path']);
|
||||
|
||||
if (preg_match($pattern, $requestUri, $matches)) {
|
||||
array_shift($matches); // Remove full match
|
||||
|
||||
// Execute middleware
|
||||
foreach ($route['middleware'] as $middleware) {
|
||||
$this->executeMiddleware($middleware);
|
||||
}
|
||||
|
||||
// Execute handler
|
||||
$this->executeHandler($route['handler'], $matches);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 404 Not Found
|
||||
http_response_code(404);
|
||||
echo "404 - Page Not Found";
|
||||
}
|
||||
|
||||
private function convertToPattern(string $path): string
|
||||
{
|
||||
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '([^/]+)', $path);
|
||||
return '#^' . $pattern . '$#';
|
||||
}
|
||||
|
||||
private function executeMiddleware(string $middleware): void
|
||||
{
|
||||
$parts = explode(':', $middleware);
|
||||
$name = $parts[0];
|
||||
$params = $parts[1] ?? null;
|
||||
|
||||
$middlewareClass = "App\\Middleware\\" . ucfirst($name) . "Middleware";
|
||||
|
||||
if (!class_exists($middlewareClass)) {
|
||||
throw new \RuntimeException("Middleware not found: {$middlewareClass}");
|
||||
}
|
||||
|
||||
$instance = new $middlewareClass();
|
||||
$instance->handle($params);
|
||||
}
|
||||
|
||||
private function executeHandler($handler, array $params): void
|
||||
{
|
||||
if (is_array($handler)) {
|
||||
[$class, $method] = $handler;
|
||||
$controller = new $class();
|
||||
call_user_func_array([$controller, $method], $params);
|
||||
} elseif (is_callable($handler)) {
|
||||
call_user_func_array($handler, $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user