Complete Phase 1: MVC, DB migrations, Auth, RBAC, Security, and Views
This commit is contained in:
123
app/Core/App.php
Normal file
123
app/Core/App.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Throwable;
|
||||
use App\Middleware\MiddlewareInterface;
|
||||
|
||||
class App
|
||||
{
|
||||
public static App $app;
|
||||
public Container $container;
|
||||
public Request $request;
|
||||
public Response $response;
|
||||
public Session $session;
|
||||
public Router $router;
|
||||
|
||||
public function __construct(Container $container)
|
||||
{
|
||||
self::$app = $this;
|
||||
$this->container = $container;
|
||||
$this->request = new Request();
|
||||
$this->response = new Response();
|
||||
$this->session = new Session();
|
||||
$this->router = new Router();
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot and run the application.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
try {
|
||||
// Match request to route
|
||||
$routeInfo = $this->router->resolve($this->request);
|
||||
$callback = $routeInfo['callback'];
|
||||
$middlewares = $routeInfo['middleware'];
|
||||
$params = $routeInfo['params'];
|
||||
|
||||
// Inject matched route parameters into Request
|
||||
$this->request->setRouteParams($params);
|
||||
|
||||
// Run Middleware Chain
|
||||
$this->executeMiddlewareChain($middlewares, function() use ($callback) {
|
||||
// Execute Route action
|
||||
if (is_callable($callback)) {
|
||||
$response = $callback($this->request, $this->response);
|
||||
} else {
|
||||
[$controllerClass, $method] = $callback;
|
||||
$controller = $this->container->get($controllerClass);
|
||||
$response = $controller->$method($this->request, $this->response);
|
||||
}
|
||||
|
||||
// Auto-output string responses as HTML
|
||||
if (is_string($response)) {
|
||||
$this->response->html($response);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Throwable $e) {
|
||||
$this->handleException($e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the array of middlewares sequentially before firing destination action.
|
||||
*/
|
||||
private function executeMiddlewareChain(array $middlewares, callable $destination): void
|
||||
{
|
||||
$runner = function (int $index) use ($middlewares, $destination, &$runner) {
|
||||
if ($index >= count($middlewares)) {
|
||||
$destination();
|
||||
return;
|
||||
}
|
||||
|
||||
$middlewareClass = $middlewares[$index];
|
||||
/** @var MiddlewareInterface $middleware */
|
||||
$middleware = $this->container->get($middlewareClass);
|
||||
|
||||
$middleware->handle($this->request, $this->response, function() use ($runner, $index) {
|
||||
$runner($index + 1);
|
||||
});
|
||||
};
|
||||
|
||||
$runner(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Global exception handler.
|
||||
*/
|
||||
private function handleException(Throwable $e): void
|
||||
{
|
||||
$code = $e->getCode();
|
||||
if ($code < 100 || $code > 599) {
|
||||
$code = 500;
|
||||
}
|
||||
|
||||
// Return API error JSON for API routes or JSON requests
|
||||
if ($this->request->isJson() || str_starts_with($this->request->getPath(), '/api')) {
|
||||
$this->response->json([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $code
|
||||
], $code);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render error templates for Web requests
|
||||
$viewPath = __DIR__ . "/../../resources/views/errors/{$code}.php";
|
||||
if (!file_exists($viewPath)) {
|
||||
$viewPath = __DIR__ . '/../../resources/views/errors/500.php';
|
||||
}
|
||||
|
||||
if (file_exists($viewPath)) {
|
||||
ob_start();
|
||||
$message = $e->getMessage();
|
||||
include $viewPath;
|
||||
$content = ob_get_clean();
|
||||
$this->response->html($content, $code);
|
||||
} else {
|
||||
$this->response->html("<h1>Error {$code}</h1><p>{$e->getMessage()}</p>", $code);
|
||||
}
|
||||
}
|
||||
}
|
||||
116
app/Core/Container.php
Normal file
116
app/Core/Container.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Exception;
|
||||
|
||||
class Container implements ContainerInterface
|
||||
{
|
||||
private array $bindings = [];
|
||||
private array $instances = [];
|
||||
|
||||
/**
|
||||
* Bind a dependency to the container.
|
||||
*/
|
||||
public function bind(string $id, callable|string $concrete, bool $singleton = false): void
|
||||
{
|
||||
$this->bindings[$id] = [
|
||||
'concrete' => $concrete,
|
||||
'singleton' => $singleton
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a singleton dependency.
|
||||
*/
|
||||
public function singleton(string $id, callable|string $concrete): void
|
||||
{
|
||||
$this->bind($id, $concrete, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a dependency from the container.
|
||||
*/
|
||||
public function get(string $id): mixed
|
||||
{
|
||||
if (isset($this->instances[$id])) {
|
||||
return $this->instances[$id];
|
||||
}
|
||||
|
||||
if (!$this->has($id)) {
|
||||
return $this->resolve($id);
|
||||
}
|
||||
|
||||
$binding = $this->bindings[$id];
|
||||
$concrete = $binding['concrete'];
|
||||
|
||||
$object = $this->resolve($concrete);
|
||||
|
||||
if ($binding['singleton']) {
|
||||
$this->instances[$id] = $object;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a dependency is bound.
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
return isset($this->bindings[$id]) || isset($this->instances[$id]) || class_exists($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a concrete class or binding.
|
||||
*/
|
||||
private function resolve(mixed $concrete): mixed
|
||||
{
|
||||
if ($concrete instanceof \Closure || is_callable($concrete)) {
|
||||
return $concrete($this);
|
||||
}
|
||||
|
||||
if (is_string($concrete)) {
|
||||
if (!class_exists($concrete)) {
|
||||
return $concrete;
|
||||
}
|
||||
|
||||
$reflector = new \ReflectionClass($concrete);
|
||||
|
||||
if (!$reflector->isInstantiable()) {
|
||||
throw new Exception("Class {$concrete} is not instantiable.");
|
||||
}
|
||||
|
||||
$constructor = $reflector->getConstructor();
|
||||
|
||||
if (null === $constructor) {
|
||||
return new $concrete();
|
||||
}
|
||||
|
||||
$parameters = $constructor->getParameters();
|
||||
$dependencies = [];
|
||||
|
||||
foreach ($parameters as $parameter) {
|
||||
$type = $parameter->getType();
|
||||
|
||||
if (!$type) {
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
$dependencies[] = $parameter->getDefaultValue();
|
||||
} else {
|
||||
throw new Exception("Cannot resolve parameter {$parameter->getName()} in class {$concrete}");
|
||||
}
|
||||
} elseif ($type instanceof \ReflectionUnionType) {
|
||||
throw new Exception("Union types in constructor injection not supported for class {$concrete}");
|
||||
} else {
|
||||
$typeName = $type->getName();
|
||||
$dependencies[] = $this->get($typeName);
|
||||
}
|
||||
}
|
||||
|
||||
return $reflector->newInstanceArgs($dependencies);
|
||||
}
|
||||
|
||||
return $concrete;
|
||||
}
|
||||
}
|
||||
215
app/Core/Request.php
Normal file
215
app/Core/Request.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Request
|
||||
{
|
||||
private array $get;
|
||||
private array $post;
|
||||
private array $server;
|
||||
private array $files;
|
||||
private array $cookies;
|
||||
private ?array $json = null;
|
||||
private array $routeParams = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->get = $_GET;
|
||||
$this->post = $_POST;
|
||||
$this->server = $_SERVER;
|
||||
$this->files = $_FILES;
|
||||
$this->cookies = $_COOKIE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request path without query string.
|
||||
*/
|
||||
public function getPath(): string
|
||||
{
|
||||
$uri = $this->server['REQUEST_URI'] ?? '/';
|
||||
$position = strpos($uri, '?');
|
||||
if ($position === false) {
|
||||
return $uri;
|
||||
}
|
||||
return substr($uri, 0, $position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request HTTP method (e.g., GET, POST, PUT, DELETE).
|
||||
*/
|
||||
public function getMethod(): string
|
||||
{
|
||||
return strtoupper($this->server['REQUEST_METHOD'] ?? 'GET');
|
||||
}
|
||||
|
||||
public function isGet(): bool
|
||||
{
|
||||
return $this->getMethod() === 'GET';
|
||||
}
|
||||
|
||||
public function isPost(): bool
|
||||
{
|
||||
return $this->getMethod() === 'POST';
|
||||
}
|
||||
|
||||
public function isPut(): bool
|
||||
{
|
||||
return $this->getMethod() === 'PUT';
|
||||
}
|
||||
|
||||
public function isDelete(): bool
|
||||
{
|
||||
return $this->getMethod() === 'DELETE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set route parameters (extracted by Router).
|
||||
*/
|
||||
public function setRouteParams(array $params): void
|
||||
{
|
||||
$this->routeParams = $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get route parameters or specific key.
|
||||
*/
|
||||
public function getRouteParams(): array
|
||||
{
|
||||
return $this->routeParams;
|
||||
}
|
||||
|
||||
public function routeParam(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->routeParams[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch GET parameter.
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->get[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch POST parameter.
|
||||
*/
|
||||
public function post(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->post[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all request parameters parsed.
|
||||
*/
|
||||
public function getBody(): array
|
||||
{
|
||||
if ($this->isJson()) {
|
||||
return $this->getJsonBody();
|
||||
}
|
||||
|
||||
$body = [];
|
||||
if ($this->isGet()) {
|
||||
foreach ($this->get as $key => $value) {
|
||||
$body[$key] = filter_input(INPUT_GET, $key, FILTER_DEFAULT);
|
||||
}
|
||||
}
|
||||
if ($this->isPost()) {
|
||||
foreach ($this->post as $key => $value) {
|
||||
$body[$key] = filter_input(INPUT_POST, $key, FILTER_DEFAULT);
|
||||
}
|
||||
}
|
||||
return $body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is JSON.
|
||||
*/
|
||||
public function isJson(): bool
|
||||
{
|
||||
$contentType = $this->server['CONTENT_TYPE'] ?? $this->server['HTTP_CONTENT_TYPE'] ?? '';
|
||||
return str_contains($contentType, 'application/json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw JSON request payload.
|
||||
*/
|
||||
public function getJsonBody(): array
|
||||
{
|
||||
if ($this->json === null) {
|
||||
$rawInput = file_get_contents('php://input');
|
||||
$this->json = json_decode($rawInput, true) ?? [];
|
||||
}
|
||||
return $this->json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific input value (works for GET, POST, and JSON).
|
||||
*/
|
||||
public function input(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$body = $this->getBody();
|
||||
return $body[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all request headers.
|
||||
*/
|
||||
public function getHeaders(): array
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($this->server as $key => $value) {
|
||||
if (str_starts_with($key, 'HTTP_')) {
|
||||
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
|
||||
$headers[$name] = $value;
|
||||
} elseif ($key === 'CONTENT_TYPE') {
|
||||
$headers['Content-Type'] = $value;
|
||||
} elseif ($key === 'CONTENT_LENGTH') {
|
||||
$headers['Content-Length'] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get specific request header.
|
||||
*/
|
||||
public function getHeader(string $name, ?string $default = null): ?string
|
||||
{
|
||||
$headers = $this->getHeaders();
|
||||
// Match case-insensitively
|
||||
foreach ($headers as $key => $val) {
|
||||
if (strtolower($key) === strtolower($name)) {
|
||||
return $val;
|
||||
}
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Client IP address.
|
||||
*/
|
||||
public function getIp(): string
|
||||
{
|
||||
return $this->server['HTTP_CLIENT_IP']
|
||||
?? $this->server['HTTP_X_FORWARDED_FOR']
|
||||
?? $this->server['REMOTE_ADDR']
|
||||
?? '127.0.0.1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get User Agent.
|
||||
*/
|
||||
public function getUserAgent(): string
|
||||
{
|
||||
return $this->server['HTTP_USER_AGENT'] ?? 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uploaded file array.
|
||||
*/
|
||||
public function file(string $key): ?array
|
||||
{
|
||||
return $this->files[$key] ?? null;
|
||||
}
|
||||
}
|
||||
89
app/Core/Response.php
Normal file
89
app/Core/Response.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Response
|
||||
{
|
||||
private int $statusCode = 200;
|
||||
private array $headers = [];
|
||||
|
||||
/**
|
||||
* Set the HTTP status code.
|
||||
*/
|
||||
public function setStatusCode(int $code): self
|
||||
{
|
||||
$this->statusCode = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the HTTP status code.
|
||||
*/
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a header.
|
||||
*/
|
||||
public function header(string $name, string $value): self
|
||||
{
|
||||
$this->headers[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set redirect path.
|
||||
*/
|
||||
public function redirect(string $url): void
|
||||
{
|
||||
header("Location: " . $url);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output JSON data.
|
||||
*/
|
||||
public function json(mixed $data, int $code = 200): void
|
||||
{
|
||||
$this->setStatusCode($code);
|
||||
$this->header('Content-Type', 'application/json; charset=utf-8');
|
||||
|
||||
$this->sendHeaders();
|
||||
http_response_code($this->statusCode);
|
||||
|
||||
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output raw HTML or string content.
|
||||
*/
|
||||
public function html(string $content, int $code = 200): void
|
||||
{
|
||||
$this->setStatusCode($code);
|
||||
if (!isset($this->headers['Content-Type'])) {
|
||||
$this->header('Content-Type', 'text/html; charset=utf-8');
|
||||
}
|
||||
|
||||
$this->sendHeaders();
|
||||
http_response_code($this->statusCode);
|
||||
|
||||
echo $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send all buffered headers.
|
||||
*/
|
||||
public function sendHeaders(): void
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->headers as $name => $value) {
|
||||
header("$name: $value");
|
||||
}
|
||||
}
|
||||
}
|
||||
113
app/Core/Router.php
Normal file
113
app/Core/Router.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Router
|
||||
{
|
||||
private array $routes = [];
|
||||
private array $groupMiddleware = [];
|
||||
private string $groupPrefix = '';
|
||||
|
||||
/**
|
||||
* Map a route.
|
||||
*/
|
||||
public function addRoute(string $method, string $path, array|callable $callback, array $middleware = []): void
|
||||
{
|
||||
$prefix = '/' . trim($this->groupPrefix, '/');
|
||||
$prefix = $prefix === '/' ? '' : $prefix;
|
||||
|
||||
$trimmedPath = '/' . trim($path, '/');
|
||||
$trimmedPath = $trimmedPath === '/' ? '' : $trimmedPath;
|
||||
|
||||
$fullPath = $prefix . $trimmedPath;
|
||||
$fullPath = $fullPath === '' ? '/' : $fullPath;
|
||||
|
||||
$this->routes[strtoupper($method)][] = [
|
||||
'path' => $fullPath,
|
||||
'callback' => $callback,
|
||||
'middleware' => array_merge($this->groupMiddleware, $middleware)
|
||||
];
|
||||
}
|
||||
|
||||
public function get(string $path, array|callable $callback, array $middleware = []): void
|
||||
{
|
||||
$this->addRoute('GET', $path, $callback, $middleware);
|
||||
}
|
||||
|
||||
public function post(string $path, array|callable $callback, array $middleware = []): void
|
||||
{
|
||||
$this->addRoute('POST', $path, $callback, $middleware);
|
||||
}
|
||||
|
||||
public function put(string $path, array|callable $callback, array $middleware = []): void
|
||||
{
|
||||
$this->addRoute('PUT', $path, $callback, $middleware);
|
||||
}
|
||||
|
||||
public function delete(string $path, array|callable $callback, array $middleware = []): void
|
||||
{
|
||||
$this->addRoute('DELETE', $path, $callback, $middleware);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group routes under attributes like prefix and middleware.
|
||||
*/
|
||||
public function group(array $attributes, callable $callback): void
|
||||
{
|
||||
$previousPrefix = $this->groupPrefix;
|
||||
$previousMiddleware = $this->groupMiddleware;
|
||||
|
||||
if (isset($attributes['prefix'])) {
|
||||
$this->groupPrefix = $previousPrefix . '/' . trim($attributes['prefix'], '/');
|
||||
}
|
||||
|
||||
if (isset($attributes['middleware'])) {
|
||||
$this->groupMiddleware = array_merge($previousMiddleware, (array)$attributes['middleware']);
|
||||
}
|
||||
|
||||
$callback($this);
|
||||
|
||||
$this->groupPrefix = $previousPrefix;
|
||||
$this->groupMiddleware = $previousMiddleware;
|
||||
}
|
||||
|
||||
/**
|
||||
* Match path against configured routes.
|
||||
*/
|
||||
public function resolve(Request $request): array
|
||||
{
|
||||
$method = $request->getMethod();
|
||||
$path = '/' . trim($request->getPath(), '/');
|
||||
|
||||
$routes = $this->routes[$method] ?? [];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$pattern = $this->compilePattern($route['path']);
|
||||
if (preg_match($pattern, $path, $matches)) {
|
||||
// Filter out non-string keys from named capture groups
|
||||
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
|
||||
return [
|
||||
'callback' => $route['callback'],
|
||||
'middleware' => $route['middleware'],
|
||||
'params' => $params
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception("Route not found", 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert route notation {param} into regex capture groups.
|
||||
*/
|
||||
private function compilePattern(string $path): string
|
||||
{
|
||||
$cleanPath = '/' . trim($path, '/');
|
||||
$cleanPath = $cleanPath === '/' ? '' : $cleanPath;
|
||||
|
||||
$pattern = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[^/]+)', $cleanPath);
|
||||
return '#^' . ($pattern === '' ? '/' : $pattern) . '$#';
|
||||
}
|
||||
}
|
||||
148
app/Core/Session.php
Normal file
148
app/Core/Session.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
class Session
|
||||
{
|
||||
private const FLASH_KEY = 'flash_messages';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Set secure session parameters
|
||||
session_start([
|
||||
'cookie_httponly' => true,
|
||||
'cookie_secure' => false, // Set to true if HTTPS is enforced (e.g. on production)
|
||||
'cookie_samesite' => 'Lax',
|
||||
]);
|
||||
}
|
||||
|
||||
// Mark existing flash messages to be deleted next request
|
||||
$this->ageFlashMessages();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a session value.
|
||||
*/
|
||||
public function set(string $key, mixed $value): void
|
||||
{
|
||||
$_SESSION[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session value.
|
||||
*/
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $_SESSION[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a session value.
|
||||
*/
|
||||
public function remove(string $key): void
|
||||
{
|
||||
unset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session key exists.
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
{
|
||||
return isset($_SESSION[$key]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a flash message (available only for the next request).
|
||||
*/
|
||||
public function setFlash(string $key, string $message): void
|
||||
{
|
||||
$_SESSION[self::FLASH_KEY][$key] = [
|
||||
'value' => $message,
|
||||
'remove' => false
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a flash message.
|
||||
*/
|
||||
public function getFlash(string $key, ?string $default = null): ?string
|
||||
{
|
||||
return $_SESSION[self::FLASH_KEY][$key]['value'] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all flash messages.
|
||||
*/
|
||||
public function getFlashes(): array
|
||||
{
|
||||
$flashes = [];
|
||||
foreach ($_SESSION[self::FLASH_KEY] ?? [] as $key => $flash) {
|
||||
$flashes[$key] = $flash['value'];
|
||||
}
|
||||
return $flashes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Age flash messages at start of request.
|
||||
*/
|
||||
private function ageFlashMessages(): void
|
||||
{
|
||||
$flashMessages = $_SESSION[self::FLASH_KEY] ?? [];
|
||||
foreach ($flashMessages as $key => &$flash) {
|
||||
if ($flash['remove']) {
|
||||
unset($flashMessages[$key]);
|
||||
} else {
|
||||
$flash['remove'] = true;
|
||||
}
|
||||
}
|
||||
$_SESSION[self::FLASH_KEY] = $flashMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate or fetch CSRF token.
|
||||
*/
|
||||
public function getCsrfToken(): string
|
||||
{
|
||||
$token = $this->get('csrf_token');
|
||||
if (!$token) {
|
||||
$token = bin2hex(random_bytes(32));
|
||||
$this->set('csrf_token', $token);
|
||||
}
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate CSRF token.
|
||||
*/
|
||||
public function validateCsrfToken(?string $token): bool
|
||||
{
|
||||
if (!$token) {
|
||||
return false;
|
||||
}
|
||||
$storedToken = $this->get('csrf_token');
|
||||
return hash_equals($storedToken, $token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the session.
|
||||
*/
|
||||
public function destroy(): void
|
||||
{
|
||||
$_SESSION = [];
|
||||
if (ini_get("session.use_cookies")) {
|
||||
$params = session_get_cookie_params();
|
||||
setcookie(
|
||||
session_name(),
|
||||
'',
|
||||
time() - 42000,
|
||||
$params["path"],
|
||||
$params["domain"],
|
||||
$params["secure"],
|
||||
$params["httponly"]
|
||||
);
|
||||
}
|
||||
session_destroy();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user