Skip to content

Commit 2d6ac28

Browse files
google-labs-jules[bot]mttk2004
authored andcommitted
refactor: Implement core architectural improvements
This commit introduces a major architectural refactoring to improve the framework's robustness and align it with modern PHP best practices, making it a better educational tool. Key changes include: - **Request/Response Objects:** Abstracted the HTTP layer by creating `Core\Http\Request` and `Core\Http\Response`. The application now uses these objects instead of relying on global variables like `$_SERVER`, `$_POST`, etc. This improves testability and code clarity. - **Service Container:** Added a simple `Core\Container` for dependency injection. Core services, such as the Router, are now registered as singletons and resolved from the container, which centralizes dependency management and increases flexibility. - **Exception Handling:** Overhauled the error handling mechanism. The Router now throws exceptions (e.g., `Core\Exception\NotFoundException`) for errors like missing routes. A centralized `Core\ExceptionHandler` catches these exceptions and renders appropriate user-friendly error pages (404, 500). Also added the corresponding view files for the error pages.
1 parent 4562b29 commit 2d6ac28

File tree

14 files changed

+281
-96
lines changed

14 files changed

+281
-96
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,4 @@ public/assets/*
2121
public/storage/*
2222

2323
.php-cs-fixer.cache
24+
server.log

app/Controllers/HomeController.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
namespace App\Controllers;
44

55
use Core\Controller;
6+
use Core\Http\Request;
7+
use Core\Http\Response;
68

79
class HomeController extends Controller
810
{
9-
public function index(): void
11+
public function index(Request $request): Response
1012
{
11-
$this->render('home', [
13+
return $this->render('home', [
1214
'title' => 'Home - PHPure',
1315
'message' => 'Welcome to PHPure Framework!',
1416
]);

app/routes.php

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
11
<?php
22

3+
use Core\App;
4+
use Core\Http\Request;
35
use Core\Http\Router;
46

5-
$router = new Router();
7+
// Resolve the router from the container
8+
$router = App::container()->resolve(Router::class);
69

7-
// TODO: Define routes here
10+
// Define routes here
811
$router->get('', ['HomeController', 'index']);
912

10-
$router->dispatch();
13+
// Create a request from globals
14+
$request = new Request();
15+
16+
// Dispatch the router and get a response
17+
$response = $router->dispatch($request);
18+
19+
// Send the response to the browser
20+
$response->send();

core/App.php

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,25 @@
77

88
class App
99
{
10+
protected static Container $container;
11+
12+
public function __construct(Container $container)
13+
{
14+
static::$container = $container;
15+
}
16+
17+
/**
18+
* Get the service container.
19+
*/
20+
public static function container(): Container
21+
{
22+
return static::$container;
23+
}
24+
1025
/**
1126
* Bootstrap the application
1227
*/
13-
public static function bootstrap(): void
28+
public function bootstrap(): void
1429
{
1530
// 1. Load environment variables
1631
loadEnv();
@@ -20,21 +35,17 @@ public static function bootstrap(): void
2035

2136
// 3. Enable Whoops if debug mode is enabled
2237
if (config('app.debug', true)) {
23-
self::enableWhoops();
38+
$this->enableWhoops();
2439
}
2540

2641
// 4. Start the session
2742
Session::start();
28-
29-
// 5. Load the event and route configuration files
30-
require_once BASE_PATH . '/app/events.php';
31-
require_once BASE_PATH . '/app/routes.php';
3243
}
3344

3445
/**
3546
* Enable Whoops
3647
*/
37-
public static function enableWhoops(): void
48+
public function enableWhoops(): void
3849
{
3950
$whoops = new Run();
4051
$whoops->pushHandler(new PrettyPageHandler());

core/Container.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
namespace Core;
4+
5+
use Closure;
6+
use Exception;
7+
8+
class Container
9+
{
10+
protected array $bindings = [];
11+
protected array $instances = [];
12+
13+
/**
14+
* Bind a resolver into the container.
15+
*
16+
* @param string $key The abstract key (e.g., 'router' or Router::class).
17+
* @param Closure|string|null $concrete The concrete resolver or class name.
18+
* @param bool $singleton Whether the binding should be a singleton.
19+
*/
20+
public function bind(string $key, Closure|string|null $concrete = null, bool $singleton = false): void
21+
{
22+
if (is_null($concrete)) {
23+
$concrete = $key;
24+
}
25+
26+
$this->bindings[$key] = compact('concrete', 'singleton');
27+
}
28+
29+
/**
30+
* Register a shared (singleton) binding in the container.
31+
*
32+
* @param string $key
33+
* @param Closure|string|null $concrete
34+
*/
35+
public function singleton(string $key, Closure|string|null $concrete = null): void
36+
{
37+
$this->bind($key, $concrete, true);
38+
}
39+
40+
/**
41+
* Resolve the given type from the container.
42+
*
43+
* @param string $key
44+
* @return mixed
45+
* @throws Exception
46+
*/
47+
public function resolve(string $key): mixed
48+
{
49+
// If the instance already exists as a singleton, return it.
50+
if (isset($this->instances[$key])) {
51+
return $this->instances[$key];
52+
}
53+
54+
if (!isset($this->bindings[$key])) {
55+
throw new Exception("No binding found for '{$key}'");
56+
}
57+
58+
$resolver = $this->bindings[$key]['concrete'];
59+
60+
// If the resolver is not a closure, it's a class name.
61+
if (!$resolver instanceof Closure) {
62+
// Here you could add auto-wiring with reflection, but for a simple container, we'll keep it basic.
63+
return new $resolver();
64+
}
65+
66+
$instance = $resolver($this);
67+
68+
// If it's a singleton, store the instance.
69+
if ($this->bindings[$key]['singleton']) {
70+
$this->instances[$key] = $instance;
71+
}
72+
73+
return $instance;
74+
}
75+
}

core/Controller.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Core;
44

5+
use Core\Http\Response;
56
use Twig\Error\LoaderError;
67
use Twig\Error\RuntimeError;
78
use Twig\Error\SyntaxError;
@@ -15,8 +16,10 @@ class Controller
1516
* @throws RuntimeError
1617
* @throws LoaderError
1718
*/
18-
protected function render(string $view, array $data = []): void
19+
protected function render(string $view, array $data = []): Response
1920
{
20-
echo Twig::getInstance()->render($view . '.html.twig', $data);
21+
$content = Twig::getInstance()->render($view . '.html.twig', $data);
22+
23+
return new Response($content);
2124
}
2225
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Core\Exception;
4+
5+
use Exception;
6+
7+
class NotFoundException extends Exception
8+
{
9+
protected $message = 'Page Not Found';
10+
protected $code = 404;
11+
}

core/ExceptionHandler.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,21 @@ public static function handleException(\Throwable $exception): void
2020
{
2121
// Log the error
2222
Logger::error($exception->getMessage(), [
23-
'file' => $exception->getFile(),
24-
'line' => $exception->getLine(),
25-
'trace' => $exception->getTraceAsString(),
23+
'file' => $exception->getFile(),
24+
'line' => $exception->getLine(),
25+
'trace' => $exception->getTraceAsString(),
2626
]);
2727

28-
// Display the error page
28+
// Handle NotFoundException specifically
29+
if ($exception instanceof \Core\Exception\NotFoundException) {
30+
http_response_code(404);
31+
// In a real app, you'd want a nice 404 page.
32+
// We assume one exists in Twig. If not, this will error.
33+
echo Twig::getInstance()->render('errors/404.html.twig', ['message' => $exception->getMessage()]);
34+
return;
35+
}
36+
37+
// Display the generic error page for all other exceptions
2938
if (getenv('APP_ENV') === 'production') {
3039
http_response_code(500);
3140
echo Twig::getInstance()->render('errors/500.html.twig');

core/Http/Request.php

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,46 @@
44

55
class Request
66
{
7-
/**
8-
* Get data from input
9-
*/
10-
public static function input(string $key, $default = null)
11-
{
12-
return $_POST[$key] ?? $_GET[$key] ?? $default;
13-
}
7+
public readonly array $query;
8+
public readonly array $data;
9+
public readonly array $server;
1410

15-
/**
16-
* Get data from query string
17-
*/
18-
public static function query(string $key, $default = null)
11+
public function __construct()
1912
{
20-
return $_GET[$key] ?? $default;
13+
$this->query = $_GET;
14+
$this->data = $_POST;
15+
$this->server = $_SERVER;
2116
}
2217

2318
/**
24-
* Get all data sent to the server
19+
* Get the request URI path.
2520
*/
26-
public static function all(): array
21+
public function getPath(): string
2722
{
28-
return array_merge($_GET, $_POST);
23+
return trim(parse_url($this->server['REQUEST_URI'], PHP_URL_PATH), '/');
2924
}
3025

3126
/**
32-
* Get the request method
27+
* Get the request method.
3328
*/
34-
public static function method(): string
29+
public function getMethod(): string
3530
{
36-
return strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET');
37-
}
38-
39-
/**
40-
* Get the URI
41-
*/
42-
public static function uri(): string
43-
{
44-
return $_SERVER['REQUEST_URI'] ?? '/';
45-
}
46-
47-
/**
48-
* Get the sanitized data
49-
*/
50-
public static function sanitize(string $key, $default = null)
51-
{
52-
$value = self::input($key, $default);
53-
if (is_string($value)) {
54-
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
55-
}
56-
57-
return $value;
31+
return $this->server['REQUEST_METHOD'];
5832
}
5933

6034
/**
61-
* Check if the request is an Ajax request
35+
* Get a value from the POST data.
6236
*/
63-
public static function isAjax(): bool
37+
public function get(string $key, $default = null)
6438
{
65-
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
66-
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
39+
return $this->data[$key] ?? $default;
6740
}
6841

6942
/**
70-
* Get the IP address of the user
43+
* Get a value from the GET query string.
7144
*/
72-
public static function ip(): string
45+
public function query(string $key, $default = null)
7346
{
74-
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
47+
return $this->query[$key] ?? $default;
7548
}
7649
}

core/Http/Response.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,57 @@
44

55
class Response
66
{
7+
protected mixed $content;
8+
protected int $statusCode;
9+
protected array $headers;
10+
11+
public function __construct(mixed $content = '', int $statusCode = 200, array $headers = [])
12+
{
13+
$this->content = $content;
14+
$this->statusCode = $statusCode;
15+
$this->headers = $headers;
16+
}
17+
718
/**
8-
* Send a JSON response
19+
* Set the HTTP status code.
920
*/
10-
public static function json(array $data, int $status = 200): void
21+
public function setStatusCode(int $code): self
1122
{
12-
http_response_code($status);
13-
header('Content-Type: application/json');
14-
echo json_encode($data);
23+
$this->statusCode = $code;
24+
return $this;
1525
}
1626

1727
/**
18-
* Redirect to a URL
28+
* Add a header to the response.
1929
*/
20-
public static function redirect(string $url, int $status = 302): void
30+
public function addHeader(string $name, string $value): self
2131
{
22-
http_response_code($status);
23-
header("Location: $url");
24-
exit;
32+
$this->headers[$name] = $value;
33+
return $this;
2534
}
2635

2736
/**
28-
* Send a response
37+
* Send the response to the client.
2938
*/
30-
public static function send(string $content, int $status = 200): void
39+
public function send(): void
3140
{
32-
http_response_code($status);
33-
echo $content;
41+
// Send status code
42+
http_response_code($this->statusCode);
43+
44+
// Send headers
45+
foreach ($this->headers as $name => $value) {
46+
header("$name: $value");
47+
}
48+
49+
// Send content
50+
echo $this->content;
3451
}
3552

3653
/**
37-
* Set a header
54+
* Factory method to create a new Response.
3855
*/
39-
public static function setHeader(string $name, string $value): void
56+
public static function create(mixed $content = '', int $statusCode = 200, array $headers = []): self
4057
{
41-
header("$name: $value");
58+
return new static($content, $statusCode, $headers);
4259
}
4360
}

0 commit comments

Comments
 (0)