PHP: OOP Practice Project
You've studied 5 lessons of object-oriented programming. Now let's weld all that knowledge together—two projects: a full user system covering inheritance/enums/traits/interfaces, and a simple framework that gives you a first taste of MVC architecture.
1. Project One: User Role Management System
Combine classes, inheritance, enums, traits, and interfaces to build a complete user role management framework.
▶ Example: Complete OOP User System
Output:
TEXT
📖 Display only
<h4>{root->getLogIdentifier()}</h4>
Permissions: " . implode(', ', root->getPermissions()) . "<br>
root<br>
PHP
<?php
// === Enums ===
enum UserRole: string {
case ADMIN = 'admin';
case EDITOR = 'editor';
case MEMBER = 'member';
case GUEST = 'guest';
}
enum UserStatus: string {
case ACTIVE = 'active';
case INACTIVE = 'inactive';
case BANNED = 'banned';
}
// === Interfaces ===
interface Loggable {
public function getLogIdentifier(): string;
}
interface Manageable {
public function canAccess(string $resource): bool;
public function getPermissions(): array;
}
// === Traits ===
trait HasTimestamps {
public readonly string $createdAt;
public readonly string $updatedAt;
public function touch(): void {
$now = date("Y-m-d H:i:s");
if (!isset($this->createdAt)) {
$this->createdAt = $now;
}
$this->updatedAt = $now;
}
}
trait ArraySerializable {
public function toArray(): array {
return get_object_vars($this);
}
}
// === Base Class ===
abstract class User implements Loggable, Manageable {
use HasTimestamps, ArraySerializable;
public function __construct(
public readonly string $id,
public string $name,
public string $email,
public UserRole $role = UserRole::MEMBER,
public UserStatus $status = UserStatus::ACTIVE,
) {
$this->touch();
}
public function getLogIdentifier(): string {
return "User#{$this->id} ({$this->name})";
}
abstract public function canAccess(string $resource): bool;
abstract public function getPermissions(): array;
abstract public function getRoleLabel(): string;
}
// === Concrete Role Classes ===
class Admin extends User {
public function __construct(
string $id, string $name, string $email
) {
parent::__construct($id, $name, $email, UserRole::ADMIN);
}
public function canAccess(string $resource): bool {
return true; // Admin can access everything
}
public function getPermissions(): array {
return ['create', 'read', 'update', 'delete', 'manage_users', 'system_config'];
}
public function getRoleLabel(): string {
return '🔧 Admin';
}
}
class Editor extends User {
public function __construct(
string $id, string $name, string $email
) {
parent::__construct($id, $name, $email, UserRole::EDITOR);
}
public function canAccess(string $resource): bool {
return in_array($resource, ['articles', 'media', 'comments']);
}
public function getPermissions(): array {
return ['create', 'read', 'update', 'manage_comments'];
}
public function getRoleLabel(): string {
return '✏️ Editor';
}
}
class Member extends User {
public function __construct(
string $id, string $name, string $email
) {
parent::__construct($id, $name, $email, UserRole::MEMBER);
}
public function canAccess(string $resource): bool {
return in_array($resource, ['articles', 'comments', 'profile']);
}
public function getPermissions(): array {
return ['read', 'comment'];
}
public function getRoleLabel(): string {
return '👤 Member';
}
}
// === Access Manager ===
class AccessManager {
public function checkAccess(User $user, string $resource): string {
if ($user->status === UserStatus::BANNED) {
return "{$user->getRoleLabel()} {$user->name}: Banned, access denied for {$resource}";
}
$allowed = $user->canAccess($resource)
? "✅ Access Granted" : "❌ Access Denied";
return "{$user->getRoleLabel()} {$user->name}: {$allowed} {$resource}";
}
public function listAllAccess(array $users, array $resources): void {
foreach ($users as $user) {
echo "<h4>{$user->getLogIdentifier()}</h4>";
echo "Permissions: " . implode(', ', $user->getPermissions()) . "<br>";
foreach ($resources as $res) {
echo $this->checkAccess($user, $res) . "<br>";
}
}
}
}
// === Test the System ===
$users = [
new Admin("u1", "John", "admin@example.com"),
new Editor("u2", "Jane", "editor@example.com"),
new Member("u3", "Bob", "member@example.com"),
];
$resources = ['articles', 'media', 'comments', 'system_config', 'manage_users'];
$manager = new AccessManager();
$manager->listAllAccess($users, $resources);
// Test enum + serialization
echo "<br>Admin data: " . json_encode($users[0]->toArray(), JSON_UNESCAPED_UNICODE);
?>
Output:
TEXT
📖 Display only
User#u1 (John)
Permissions: create, read, update, delete, manage_users, system_config
Access Granted: articles
Access Granted: media
2. Project Two: Simple MVC Framework
Understand the MVC concept of Router, Controller, and View—this is the core idea behind frameworks like Laravel.
▶ Example: Mini MVC
Output:
TEXT
📖 Display only
value
<h2>404 — Page Not Found</h2>
{"name":"Alice","age":25}
PHP
<?php
// === Router ===
class Router {
private array $routes = [];
public function get(string $path, callable $handler): void {
$this->routes['GET'][$path] = $handler;
}
public function post(string $path, callable $handler): void {
$this->routes['POST'][$path] = $handler;
}
public function dispatch(string $method, string $uri): void {
$path = parse_url($uri, PHP_URL_PATH);
$handler = $this->routes[$method][$path] ?? null;
if ($handler) {
echo $handler($_GET);
} else {
http_response_code(404);
echo "<h2>404 — Page Not Found</h2>";
}
}
}
// === View Base ===
class View {
public static function render(string $template, array $data = []): string {
extract($data);
ob_start();
include __DIR__ . "/{$template}.php";
return ob_get_clean();
}
}
// === Controller Base ===
abstract class Controller {
protected function json(array $data, int $code = 200): void {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($data, JSON_UNESCAPED_UNICODE);
}
}
// === Concrete Controller ===
class HomeController extends Controller {
public function index(): void {
echo View::render('home', [
'title' => 'Home',
'message' => 'Welcome to my blog!',
]);
}
public function about(): void {
echo View::render('about', [
'title' => 'About Us',
'team' => ['John', 'Jane', 'Bob'],
'version' => '1.0.0',
]);
}
public function api(): void {
$this->json([
'status' => 'ok',
'data' => ['version' => '1.0.0', 'uptime' => time()],
]);
}
}
// === Set Up Routes ===
$router = new Router();
$home = new HomeController();
$router->get('/', fn() => $home->index());
$router->get('/about', fn() => $home->about());
$router->get('/api/status', fn() => $home->api());
// === Launch the App ===
$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
?>
Output:
TEXT
📖 Display only
<h1>Welcome to MyBlog</h1> (or 404 Page Not Found for unknown routes)
The accompanying view file home.php:
PHP
<!DOCTYPE html>
<html lang="en">
<head><title><?= $title ?></title></head>
<body>
<h1><?= $message ?></h1>
<p>This page is built using MVC principles.</p>
</body>
</html>
3. The Mental Shift: From Procedural to OOP
| Procedural Thinking | OOP Thinking |
|---|---|
"I need to read user data" → write a getUser() function |
"What is a user? What are its properties and behaviors?" → design a User class |
| Data and functions scattered across files | Data + behavior encapsulated in a class |
| Code is duplicated → copy-paste or extract a function | Code is duplicated → use inheritance or Traits |
Pass 'admin' strings for permissions |
Pass UserRole::ADMIN enum, type-safe |
💡 Tip: OOP is a means, not an end. Procedural is perfectly fine for small scripts. Shift to OOP when you see these signs: (1) the same parameters keep getting passed between functions; (2) global variables are piling up; (3) conditionals are based on "type strings" (
if ($role === 'admin')).
▶ Example: Simple Dependency Injection Container
Output:
TEXT
📖 Display only
(no visible output - class defined)
PHP
<?php
class Container {
private array $bindings = [];
public function bind(string $abstract, callable $concrete): void {
$this->bindings[$abstract] = $concrete;
}
public function resolve(string $abstract): object {
if (!isset($this->bindings[$abstract])) {
throw new Exception("No binding for {$abstract}");
}
return ($this->bindings[$abstract])($this);
}
}
$container = new Container();
$container->bind(PDO::class, fn() => new PDO('sqlite::memory:'));
$container->bind(UserRepository::class, fn($c) => new UserRepository($c->resolve(PDO::class)));
$repo = $container->resolve(UserRepository::class);
?>
Output:
TEXT
📖 Display only
Output displayed
❓ FAQ
Q What does a Controller actually do in MVC?
A The Controller is the "traffic director"—it receives the request, calls the Model to get data, and chooses the View for rendering. It doesn't handle business logic itself (that's the Model's job) and doesn't write HTML (that's the View's job).
Q Should I build my own framework or use an existing one?
A Build a mini-framework during the learning phase to understand the principles (just like this lesson). Use Laravel/Symfony for production. After building your own Router/Controller/View once, using Laravel makes you say, "Ah, so that's what's happening behind the scenes."
❓ FAQ
Q How do I test the MVC mini-framework I built?
A Start Apache or the built-in PHP server (php -S localhost:8000) and navigate to http://localhost:8000 in your browser. The dispatcher routes to the appropriate controller based on the URL. Use var_dump() or error_log() to debug step by step.
📖 Summary
- The user system combines all five major OOP mechanisms: enums, inheritance, interfaces, traits, and polymorphism
abstract class Userdefines the user blueprint; subclasses implement specific role capabilitiesAccessManageruses polymorphism to uniformly check permissions across different roles- MVC principle: Router dispatches requests → Controller coordinates → View renders
- OOP isn't a silver bullet—consider it when your code exceeds 500 lines, has repeated logic, or depends on global variables
📝 Exercises
- Based on the user system, add a
Guestrole (can only access articles, no editing permissions at all) and add it to the test suite. - Add an
addRoute('GET/POST', $path, $handler)method to the router so a single call can register both methods. - Add a
UserControllerto the MVC framework that implements a user list (read from an array) and a user detail page (query by ID).