Codeskill

Learn to code, step by step

Mini project: multi-user app with roles

This mini project extends what you built in intermediate – a multi-user app with proper role separation, middleware stacks, and domain services. Plain PHP, no framework. Storage can be MySQL (from intermediate) or JSON files if you want to focus on structure without schema work.

What you are building

  • Three roles: viewer, editor, admin
  • Editors create and edit documents; viewers read only; admins manage users and roles
  • Middleware pipeline: session, CSRF, auth, role gate per route group
  • Domain services for “promote user”, “archive document” – not fat controllers
  • Audit log (who did what, when) – append-only file or table row

Route map

GET  /login, POST /login, POST /logout
GET  /documents              [auth: viewer+]
GET  /documents/create       [auth: editor+]
POST /documents              [auth: editor+, csrf]
GET  /documents/{id}/edit    [auth: editor+, owner or admin]
POST /documents/{id}         [auth: editor+, csrf]
GET  /admin/users            [auth: admin]
POST /admin/users/{id}/role  [auth: admin, csrf]

Role middleware

final class RequireRole implements Middleware
{
    /** @param list<string> $allowed */
    public function __construct(private array $allowed) {}

    public function handle(Request $request, callable $next): void
    {
        $role = $_SESSION['role'] ?? '';
        if (!in_array($role, $this->allowed, true)) {
            http_response_code(403);
            echo 'Forbidden';
            return;
        }
        $next();
    }
}

Promote user use case

final class ChangeUserRole
{
    public function __construct(
        private UserRepository $users,
        private AuditLog $audit,
    ) {}

    public function handle(int $targetId, string $newRole, int $actorId): void
    {
        if (!in_array($newRole, ['viewer', 'editor', 'admin'], true)) {
            throw new InvalidArgumentException('Invalid role.');
        }
        if ($targetId === $actorId) {
            throw new ForbiddenException('Cannot change your own role.');
        }
        $this->users->updateRole($targetId, $newRole);
        $this->audit->record($actorId, 'user.role_changed', ['target' => $targetId, 'role' => $newRole]);
    }
}

Build order

  • 1. Copy intermediate project layout; add Domain/, Application/ folders
  • 2. Implement pipeline and register global + route middleware
  • 3. Seed three test users (one per role)
  • 4. Documents CRUD with owner checks in the use case layer
  • 5. Admin user list and role change form
  • 6. Audit log on role changes and document deletes
  • 7. PHPUnit tests for ChangeUserRole and document permissions

Done when

A viewer hitting /documents/create gets 403. An editor cannot promote themselves. Admin actions appear in the audit log. You can explain where each rule lives without opening a controller.

PreviousDeployment basics – PHP-FPM, env, migrations mindset