PHP Lesson 5: Advanced Security, Performance & Building a SaaS Application

PHP Lesson 5: Advanced Security, Performance & Building a SaaS Application

Oct 31, 2025 - 13:17
 0  133679
PHP Lesson 5: Advanced Security, Performance & Building a SaaS Application

PHP Lesson 5: Advanced Security, Performance & Building a SaaS Application

Welcome to Lesson 5! Now we're diving into enterprise-level PHP development with advanced security, performance optimization, and building a complete Software-as-a-Service (SaaS) application.

Part 1: Advanced Security Practices

1.1 Comprehensive Security Class

php
<?php
// security/Security.php
class Security {
    
    // CSRF Protection
    public static function generateCSRFToken() {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public static function validateCSRFToken($token) {
        return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
    }
    
    // XSS Prevention
    public static function sanitizeOutput($data) {
        return htmlspecialchars($data, ENT_QUOTES | ENT_HTML5, 'UTF-8');
    }
    
    public static function sanitizeInput($data) {
        if (is_array($data)) {
            return array_map([self::class, 'sanitizeInput'], $data);
        }
        return trim(strip_tags($data));
    }
    
    // SQL Injection Prevention
    public static function validateEmail($email) {
        return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    public static function validateURL($url) {
        return filter_var($url, FILTER_VALIDATE_URL) !== false;
    }
    
    public static function validateInteger($value) {
        return filter_var($value, FILTER_VALIDATE_INT) !== false;
    }
    
    // Password Hashing
    public static function hashPassword($password) {
        return password_hash($password, PASSWORD_DEFAULT, ['cost' => 12]);
    }
    
    public static function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }
    
    // Rate Limiting
    public static function checkRateLimit($identifier, $maxAttempts = 5, $timeWindow = 900) {
        $key = "rate_limit_{$identifier}";
        
        if (!isset($_SESSION[$key])) {
            $_SESSION[$key] = [
                'attempts' => 0,
                'reset_time' => time() + $timeWindow
            ];
        }
        
        // Reset if time window passed
        if (time() > $_SESSION[$key]['reset_time']) {
            $_SESSION[$key] = [
                'attempts' => 0,
                'reset_time' => time() + $timeWindow
            ];
        }
        
        if ($_SESSION[$key]['attempts'] >= $maxAttempts) {
            return false; // Rate limited
        }
        
        $_SESSION[$key]['attempts']++;
        return true;
    }
    
    // File Upload Security
    public static function validateUploadedFile($file, $allowedTypes = ['jpg', 'png', 'pdf'], $maxSize = 5242880) {
        $errors = [];
        
        // Check for upload errors
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors[] = "Upload error: " . $file['error'];
            return [false, $errors];
        }
        
        // Check file size
        if ($file['size'] > $maxSize) {
            $errors[] = "File too large. Maximum size: " . ($maxSize / 1024 / 1024) . "MB";
        }
        
        // Check file type
        $fileExtension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        if (!in_array($fileExtension, $allowedTypes)) {
            $errors[] = "File type not allowed. Allowed types: " . implode(', ', $allowedTypes);
        }
        
        // Check MIME type
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        $allowedMimeTypes = [
            'jpg' => 'image/jpeg',
            'png' => 'image/png',
            'pdf' => 'application/pdf'
        ];
        
        if (!in_array($mimeType, $allowedMimeTypes)) {
            $errors[] = "Invalid file type detected.";
        }
        
        return [empty($errors), $errors];
    }
    
    // Input Validation Rules
    public static function validateData($data, $rules) {
        $errors = [];
        
        foreach ($rules as $field => $ruleSet) {
            $value = $data[$field] ?? '';
            $rules = explode('|', $ruleSet);
            
            foreach ($rules as $rule) {
                if ($rule === 'required' && empty($value)) {
                    $errors[$field][] = "The $field field is required.";
                }
                
                if (strpos($rule, 'min:') === 0) {
                    $min = (int) substr($rule, 4);
                    if (strlen($value) < $min) {
                        $errors[$field][] = "The $field must be at least $min characters.";
                    }
                }
                
                if (strpos($rule, 'max:') === 0) {
                    $max = (int) substr($rule, 4);
                    if (strlen($value) > $max) {
                        $errors[$field][] = "The $field may not be greater than $max characters.";
                    }
                }
                
                if ($rule === 'email' && !self::validateEmail($value)) {
                    $errors[$field][] = "The $field must be a valid email address.";
                }
                
                if ($rule === 'numeric' && !is_numeric($value)) {
                    $errors[$field][] = "The $field must be a number.";
                }
            }
        }
        
        return $errors;
    }
}

// security/Encryption.php
class Encryption {
    private static $method = 'aes-256-cbc';
    
    public static function encrypt($data, $key) {
        $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length(self::$method));
        $encrypted = openssl_encrypt($data, self::$method, $key, 0, $iv);
        return base64_encode($encrypted . '::' . $iv);
    }
    
    public static function decrypt($data, $key) {
        list($encrypted_data, $iv) = explode('::', base64_decode($data), 2);
        return openssl_decrypt($encrypted_data, self::$method, $key, 0, $iv);
    }
    
    public static function generateKey() {
        return bin2hex(random_bytes(32));
    }
}
?>

1.2 Secure Authentication System

php
<?php
// auth/SecureAuth.php
session_start();

class SecureAuth {
    private $db;
    private $maxLoginAttempts = 5;
    private $lockoutTime = 900; // 15 minutes
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    public function register($userData) {
        // Validate input
        $rules = [
            'name' => 'required|min:2|max:50',
            'email' => 'required|email',
            'password' => 'required|min:8'
        ];
        
        $errors = Security::validateData($userData, $rules);
        
        if (!empty($errors)) {
            return ['success' => false, 'errors' => $errors];
        }
        
        // Check if email exists
        if ($this->emailExists($userData['email'])) {
            return ['success' => false, 'errors' => ['email' => ['Email already registered']]];
        }
        
        try {
            $query = "INSERT INTO users (name, email, password, role, created_at) 
                     VALUES (:name, :email, :password, 'user', NOW())";
            $stmt = $this->db->prepare($query);
            
            $stmt->bindParam(':name', Security::sanitizeInput($userData['name']));
            $stmt->bindParam(':email', Security::sanitizeInput($userData['email']));
            $stmt->bindParam(':password', Security::hashPassword($userData['password']));
            
            if ($stmt->execute()) {
                // Log the registration
                $this->logSecurityEvent($this->db->lastInsertId(), 'registration', 'User registered successfully');
                
                return ['success' => true, 'message' => 'Registration successful'];
            }
        } catch (PDOException $e) {
            error_log("Registration error: " . $e->getMessage());
            return ['success' => false, 'errors' => ['general' => ['Registration failed. Please try again.']]];
        }
    }
    
    public function login($email, $password) {
        // Check rate limiting
        if (!$this->checkLoginAttempts($email)) {
            return ['success' => false, 'errors' => ['general' => ['Too many login attempts. Please try again later.']]];
        }
        
        try {
            $query = "SELECT * FROM users WHERE email = :email AND status = 'active'";
            $stmt = $this->db->prepare($query);
            $stmt->bindParam(':email', $email);
            $stmt->execute();
            
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if ($user && Security::verifyPassword($password, $user['password'])) {
                // Successful login
                $this->clearLoginAttempts($email);
                
                // Update last login
                $this->updateLastLogin($user['id']);
                
                // Create session
                $this->createUserSession($user);
                
                // Log successful login
                $this->logSecurityEvent($user['id'], 'login_success', 'User logged in successfully');
                
                return ['success' => true, 'user' => $user];
            } else {
                // Failed login
                $this->recordFailedAttempt($email);
                $this->logSecurityEvent($user['id'] ?? null, 'login_failed', 'Failed login attempt for email: ' . $email);
                
                return ['success' => false, 'errors' => ['general' => ['Invalid email or password']]];
            }
        } catch (PDOException $e) {
            error_log("Login error: " . $e->getMessage());
            return ['success' => false, 'errors' => ['general' => ['Login failed. Please try again.']]];
        }
    }
    
    private function checkLoginAttempts($email) {
        $key = "login_attempts_" . md5($email);
        
        if (!isset($_SESSION[$key])) {
            $_SESSION[$key] = [
                'attempts' => 0,
                'last_attempt' => time()
            ];
        }
        
        // Reset attempts if lockout time passed
        if (time() - $_SESSION[$key]['last_attempt'] > $this->lockoutTime) {
            $_SESSION[$key] = [
                'attempts' => 0,
                'last_attempt' => time()
            ];
        }
        
        if ($_SESSION[$key]['attempts'] >= $this->maxLoginAttempts) {
            return false;
        }
        
        return true;
    }
    
    private function recordFailedAttempt($email) {
        $key = "login_attempts_" . md5($email);
        $_SESSION[$key]['attempts']++;
        $_SESSION[$key]['last_attempt'] = time();
    }
    
    private function clearLoginAttempts($email) {
        $key = "login_attempts_" . md5($email);
        unset($_SESSION[$key]);
    }
    
    private function createUserSession($user) {
        $_SESSION['user'] = [
            'id' => $user['id'],
            'name' => $user['name'],
            'email' => $user['email'],
            'role' => $user['role'],
            'last_login' => $user['last_login'],
            'session_id' => session_id(),
            'ip_address' => $_SERVER['REMOTE_ADDR'],
            'user_agent' => $_SERVER['HTTP_USER_AGENT']
        ];
        
        // Regenerate session ID to prevent fixation
        session_regenerate_id(true);
    }
    
    private function emailExists($email) {
        $query = "SELECT id FROM users WHERE email = :email";
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':email', $email);
        $stmt->execute();
        return $stmt->rowCount() > 0;
    }
    
    private function updateLastLogin($userId) {
        $query = "UPDATE users SET last_login = NOW() WHERE id = :id";
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':id', $userId);
        $stmt->execute();
    }
    
    private function logSecurityEvent($userId, $eventType, $description) {
        $query = "INSERT INTO security_logs (user_id, event_type, description, ip_address, user_agent, created_at) 
                 VALUES (:user_id, :event_type, :description, :ip_address, :user_agent, NOW())";
        $stmt = $this->db->prepare($query);
        
        $stmt->bindParam(':user_id', $userId);
        $stmt->bindParam(':event_type', $eventType);
        $stmt->bindParam(':description', $description);
        $stmt->bindParam(':ip_address', $_SERVER['REMOTE_ADDR']);
        $stmt->bindParam(':user_agent', $_SERVER['HTTP_USER_AGENT']);
        
        $stmt->execute();
    }
    
    public function validateSession() {
        if (!isset($_SESSION['user'])) {
            return false;
        }
        
        // Check session hijacking
        if ($_SESSION['user']['ip_address'] !== $_SERVER['REMOTE_ADDR'] ||
            $_SESSION['user']['user_agent'] !== $_SERVER['HTTP_USER_AGENT']) {
            $this->logout();
            return false;
        }
        
        // Check if user still exists and is active
        $query = "SELECT id FROM users WHERE id = :id AND status = 'active'";
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':id', $_SESSION['user']['id']);
        $stmt->execute();
        
        if ($stmt->rowCount() === 0) {
            $this->logout();
            return false;
        }
        
        return true;
    }
    
    public function logout() {
        // Log logout event
        if (isset($_SESSION['user']['id'])) {
            $this->logSecurityEvent($_SESSION['user']['id'], 'logout', 'User logged out');
        }
        
        // Clear session
        session_unset();
        session_destroy();
        session_start(); // Start fresh session
    }
}
?>

Part 2: Performance Optimization & Caching

2.1 Advanced Caching System

php
<?php
// cache/CacheManager.php
class CacheManager {
    private $cacheDir;
    private $defaultTTL = 3600; // 1 hour
    
    public function __construct($cacheDir = 'cache/') {
        $this->cacheDir = $cacheDir;
        if (!file_exists($this->cacheDir)) {
            mkdir($this->cacheDir, 0755, true);
        }
    }
    
    public function get($key) {
        $filename = $this->getFilename($key);
        
        if (!file_exists($filename)) {
            return null;
        }
        
        $data = unserialize(file_get_contents($filename));
        
        // Check expiration
        if (time() > $data['expires']) {
            $this->delete($key);
            return null;
        }
        
        return $data['value'];
    }
    
    public function set($key, $value, $ttl = null) {
        if ($ttl === null) {
            $ttl = $this->defaultTTL;
        }
        
        $filename = $this->getFilename($key);
        $data = [
            'value' => $value,
            'expires' => time() + $ttl,
            'created' => time()
        ];
        
        return file_put_contents($filename, serialize($data), LOCK_EX);
    }
    
    public function delete($key) {
        $filename = $this->getFilename($key);
        if (file_exists($filename)) {
            return unlink($filename);
        }
        return true;
    }
    
    public function clear() {
        $files = glob($this->cacheDir . '*.cache');
        foreach ($files as $file) {
            if (is_file($file)) {
                unlink($file);
            }
        }
    }
    
    public function getStats() {
        $files = glob($this->cacheDir . '*.cache');
        $stats = [
            'total_files' => count($files),
            'total_size' => 0,
            'expired_files' => 0
        ];
        
        foreach ($files as $file) {
            $stats['total_size'] += filesize($file);
            $data = unserialize(file_get_contents($file));
            if (time() > $data['expires']) {
                $stats['expired_files']++;
            }
        }
        
        $stats['total_size'] = round($stats['total_size'] / 1024 / 1024, 2) . ' MB';
        
        return $stats;
    }
    
    private function getFilename($key) {
        return $this->cacheDir . md5($key) . '.cache';
    }
}

// cache/DatabaseCache.php
class DatabaseCache {
    private $db;
    private $table = 'cache';
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    public function get($key) {
        $query = "SELECT value, expires FROM {$this->table} WHERE cache_key = :key AND expires > NOW()";
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':key', $key);
        $stmt->execute();
        
        $result = $stmt->fetch(PDO::FETCH_ASSOC);
        
        if ($result) {
            return unserialize($result['value']);
        }
        
        return null;
    }
    
    public function set($key, $value, $ttl = 3600) {
        $query = "REPLACE INTO {$this->table} (cache_key, value, expires) VALUES (:key, :value, DATE_ADD(NOW(), INTERVAL :ttl SECOND))";
        $stmt = $this->db->prepare($query);
        
        $stmt->bindParam(':key', $key);
        $stmt->bindParam(':value', serialize($value));
        $stmt->bindParam(':ttl', $ttl);
        
        return $stmt->execute();
    }
    
    public function delete($key) {
        $query = "DELETE FROM {$this->table} WHERE cache_key = :key";
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':key', $key);
        return $stmt->execute();
    }
    
    public function clearExpired() {
        $query = "DELETE FROM {$this->table} WHERE expires <= NOW()";
        $stmt = $this->db->prepare($query);
        return $stmt->execute();
    }
}

// performance/QueryOptimizer.php
class QueryOptimizer {
    private $db;
    private $queryLog = [];
    
    public function __construct($db) {
        $this->db = $db;
    }
    
    public function executeOptimizedQuery($query, $params = []) {
        $startTime = microtime(true);
        
        try {
            $stmt = $this->db->prepare($query);
            $stmt->execute($params);
            $result = $stmt->fetchAll(PDO::FETCH_ASSOC);
            
            $executionTime = microtime(true) - $startTime;
            
            // Log query performance
            $this->logQuery($query, $params, $executionTime);
            
            return $result;
        } catch (PDOException $e) {
            error_log("Query error: " . $e->getMessage());
            return false;
        }
    }
    
    private function logQuery($query, $params, $executionTime) {
        $this->queryLog[] = [
            'query' => $query,
            'params' => $params,
            'execution_time' => $executionTime,
            'timestamp' => date('Y-m-d H:i:s')
        ];
        
        // Keep only last 100 queries in memory
        if (count($this->queryLog) > 100) {
            array_shift($this->queryLog);
        }
    }
    
    public function getSlowQueries($threshold = 0.1) {
        return array_filter($this->queryLog, function($log) use ($threshold) {
            return $log['execution_time'] > $threshold;
        });
    }
    
    public function getQueryStats() {
        $totalQueries = count($this->queryLog);
        $totalTime = array_sum(array_column($this->queryLog, 'execution_time'));
        $averageTime = $totalQueries > 0 ? $totalTime / $totalQueries : 0;
        
        return [
            'total_queries' => $totalQueries,
            'total_execution_time' => round($totalTime, 4),
            'average_execution_time' => round($averageTime, 4),
            'slow_queries' => count($this->getSlowQueries())
        ];
    }
}
?>

Part 3: Complete SaaS Application - Project Management Tool

3.1 SaaS Database Schema

sql
-- saas_database_setup.sql
CREATE DATABASE IF NOT EXISTS saas_project_manager;
USE saas_project_manager;

-- Users table (multi-tenant)
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    tenant_id VARCHAR(50) NOT NULL,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    role ENUM('owner', 'admin', 'member', 'viewer') DEFAULT 'member',
    status ENUM('active', 'inactive', 'suspended') DEFAULT 'active',
    last_login DATETIME,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_tenant (tenant_id),
    INDEX idx_email (email)
);

-- Tenants/Organizations
CREATE TABLE tenants (
    id VARCHAR(50) PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    plan ENUM('free', 'pro', 'enterprise') DEFAULT 'free',
    max_users INT DEFAULT 5,
    max_projects INT DEFAULT 10,
    status ENUM('active', 'suspended', 'cancelled') DEFAULT 'active',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    trial_ends_at TIMESTAMP NULL
);

-- Projects
CREATE TABLE projects (
    id INT AUTO_INCREMENT PRIMARY KEY,
    tenant_id VARCHAR(50) NOT NULL,
    name VARCHAR(100) NOT NULL,
    description TEXT,
    status ENUM('planning', 'active', 'on_hold', 'completed', 'cancelled') DEFAULT 'planning',
    start_date DATE,
    end_date DATE,
    budget DECIMAL(10,2) DEFAULT 0.00,
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (created_by) REFERENCES users(id),
    INDEX idx_tenant_status (tenant_id, status)
);

-- Tasks
CREATE TABLE tasks (
    id INT AUTO_INCREMENT PRIMARY KEY,
    project_id INT NOT NULL,
    title VARCHAR(200) NOT NULL,
    description TEXT,
    status ENUM('todo', 'in_progress', 'review', 'done') DEFAULT 'todo',
    priority ENUM('low', 'medium', 'high', 'urgent') DEFAULT 'medium',
    assigned_to INT,
    due_date DATE,
    estimated_hours DECIMAL(4,2) DEFAULT 0.00,
    actual_hours DECIMAL(4,2) DEFAULT 0.00,
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE,
    FOREIGN KEY (assigned_to) REFERENCES users(id),
    FOREIGN KEY (created_by) REFERENCES users(id),
    INDEX idx_project_status (project_id, status),
    INDEX idx_assigned (assigned_to)
);

-- Time tracking
CREATE TABLE time_entries (
    id INT AUTO_INCREMENT PRIMARY KEY,
    task_id INT NOT NULL,
    user_id INT NOT NULL,
    description TEXT,
    hours DECIMAL(4,2) NOT NULL,
    entry_date DATE NOT NULL,
    billed BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_user_date (user_id, entry_date)
);

-- File attachments
CREATE TABLE files (
    id INT AUTO_INCREMENT PRIMARY KEY,
    tenant_id VARCHAR(50) NOT NULL,
    name VARCHAR(255) NOT NULL,
    path VARCHAR(500) NOT NULL,
    mime_type VARCHAR(100),
    size INT,
    uploaded_by INT,
    task_id INT,
    project_id INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (uploaded_by) REFERENCES users(id),
    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE SET NULL,
    FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE SET NULL
);

-- Cache table
CREATE TABLE cache (
    cache_key VARCHAR(255) PRIMARY KEY,
    value LONGTEXT NOT NULL,
    expires DATETIME NOT NULL,
    INDEX idx_expires (expires)
);

-- Security logs
CREATE TABLE security_logs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    event_type VARCHAR(50) NOT NULL,
    description TEXT,
    ip_address VARCHAR(45),
    user_agent TEXT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    INDEX idx_user_event (user_id, event_type)
);

3.2 SaaS Application Core

php
<?php
// saas/Core.php
class SaaSApp {
    private $db;
    private $cache;
    private $currentTenant;
    private $currentUser;
    
    public function __construct($db) {
        $this->db = $db;
        $this->cache = new DatabaseCache($db);
        $this->initialize();
    }
    
    private function initialize() {
        session_start();
        
        // Set security headers
        $this->setSecurityHeaders();
        
        // Initialize tenant context
        $this->detectTenant();
        
        // Initialize user session
        $this->initializeUser();
    }
    
    private function setSecurityHeaders() {
        header('X-Frame-Options: DENY');
        header('X-XSS-Protection: 1; mode=block');
        header('X-Content-Type-Options: nosniff');
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
        header('Content-Security-Policy: default-src \'self\'; script-src \'self\' \'unsafe-inline\'; style-src \'self\' \'unsafe-inline\';');
    }
    
    private function detectTenant() {
        // For multi-tenant SaaS, detect tenant from subdomain or URL
        $host = $_SERVER['HTTP_HOST'];
        $subdomain = explode('.', $host)[0];
        
        // In production, you'd validate this against the database
        $this->currentTenant = $subdomain;
    }
    
    private function initializeUser() {
        if (isset($_SESSION['user']) && isset($_SESSION['user']['tenant_id'])) {
            $auth = new SecureAuth($this->db);
            if ($auth->validateSession()) {
                $this->currentUser = $_SESSION['user'];
            } else {
                $this->currentUser = null;
            }
        }
    }
    
    public function requireAuth($requiredRole = null) {
        if (!$this->currentUser) {
            header('Location: /login.php');
            exit;
        }
        
        if ($requiredRole && !$this->hasPermission($requiredRole)) {
            $this->showError('Access denied. Insufficient permissions.');
            exit;
        }
    }
    
    public function hasPermission($requiredRole) {
        $roleHierarchy = [
            'viewer' => 1,
            'member' => 2,
            'admin' => 3,
            'owner' => 4
        ];
        
        $userLevel = $roleHierarchy[$this->currentUser['role']] ?? 0;
        $requiredLevel = $roleHierarchy[$requiredRole] ?? 0;
        
        return $userLevel >= $requiredLevel;
    }
    
    public function getCachedData($key, $callback, $ttl = 3600) {
        $cached = $this->cache->get($key);
        
        if ($cached !== null) {
            return $cached;
        }
        
        $data = $callback();
        $this->cache->set($key, $data, $ttl);
        
        return $data;
    }
    
    public function showError($message, $code = 400) {
        http_response_code($code);
        
        if ($this->isAjaxRequest()) {
            echo json_encode(['error' => $message]);
        } else {
            // Render error page
            echo "<!DOCTYPE html>
            <html>
            <head>
                <title>Error</title>
                <style>
                    body { font-family: Arial, sans-serif; max-width: 600px; margin: 100px auto; padding: 20px; text-align: center; }
                    .error { background: #f8d7da; color: #721c24; padding: 20px; border-radius: 5px; }
                </style>
            </head>
            <body>
                <div class='error'>
                    <h2>Error</h2>
                    <p>$message</p>
                    <a href='/dashboard.php'>Return to Dashboard</a>
                </div>
            </body>
            </html>";
        }
        exit;
    }
    
    private function isAjaxRequest() {
        return !empty($_SERVER['HTTP_X_REQUESTED_WITH']) && 
               strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest';
    }
    
    public function getCurrentUser() {
        return $this->currentUser;
    }
    
    public function getCurrentTenant() {
        return $this->currentTenant;
    }
}

// saas/ProjectManager.php
class ProjectManager {
    private $db;
    private $app;
    
    public function __construct($db, $app) {
        $this->db = $db;
        $this->app = $app;
    }
    
    public function createProject($data) {
        $user = $this->app->getCurrentUser();
        $tenant = $this->app->getCurrentTenant();
        
        $query = "INSERT INTO projects (tenant_id, name, description, status, start_date, end_date, budget, created_by) 
                 VALUES (:tenant_id, :name, :description, :status, :start_date, :end_date, :budget, :created_by)";
        
        $stmt = $this->db->prepare($query);
        $stmt->bindParam(':tenant_id', $tenant);
        $stmt->bindParam(':name', $data['name']);
        $stmt->bindParam(':description', $data['description']);
        $stmt->bindParam(':status', $data['status']);
        $stmt->bindParam(':start_date', $data['start_date']);
        $stmt->bindParam(':end_date', $data['end_date']);
        $stmt->bindParam(':budget', $data['budget']);
        $stmt->bindParam(':created_by', $user['id']);
        
        if ($stmt->execute()) {
            $projectId = $this->db->lastInsertId();
            
            // Clear projects cache
            $this->app->getCachedData("projects_{$tenant}", function() {}, 0);
            
            return $projectId;
        }
        
        return false;
    }
    
    public function getProjects($filters = []) {
        $tenant = $this->app->getCurrentTenant();
        
        return $this->app->getCachedData("projects_{$tenant}_" . md5(serialize($filters)), function() use ($tenant, $filters) {
            $query = "SELECT p.*, u.name as created_by_name, 
                     COUNT(t.id) as task_count,
                     COUNT(CASE WHEN t.status = 'done' THEN 1 END) as completed_tasks
                     FROM projects p
                     LEFT JOIN users u ON p.created_by = u.id
                     LEFT JOIN tasks t ON p.id = t.project_id
                     WHERE p.tenant_id = :tenant_id";
            
            $params = [':tenant_id' => $tenant];
            
            if (!empty($filters['status'])) {
                $query .= " AND p.status = :status";
                $params[':status'] = $filters['status'];
            }
            
            $query .= " GROUP BY p.id ORDER BY p.created_at DESC";
            
            $stmt = $this->db->prepare($query);
            $stmt->execute($params);
            
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        }, 300); // Cache for 5 minutes
    }
    
    public function getProjectStats($projectId) {
        return $this->app->getCachedData("project_stats_{$projectId}", function() use ($projectId) {
            $query = "SELECT 
                     COUNT(*) as total_tasks,
                     COUNT(CASE WHEN status = 'done' THEN 1 END) as completed_tasks,
                     COUNT(CASE WHEN status = 'in_progress' THEN 1 END) as in_progress_tasks,
                     SUM(estimated_hours) as total_estimated_hours,
                     SUM(actual_hours) as total_actual_hours,
                     COUNT(CASE WHEN due_date < CURDATE() AND status != 'done' THEN 1 END) as overdue_tasks
                     FROM tasks WHERE project_id = :project_id";
            
            $stmt = $this->db->prepare($query);
            $stmt->bindParam(':project_id', $projectId);
            $stmt->execute();
            
            return $stmt->fetch(PDO::FETCH_ASSOC);
        }, 60); // Cache for 1 minute
    }
}
?>

3.3 SaaS Dashboard

php
<?php
// dashboard.php
require_once 'config/Database.php';
require_once 'saas/Core.php';
require_once 'saas/ProjectManager.php';

$database = new Database();
$db = $database->getConnection();
$app = new SaaSApp($db);

// Require authentication
$app->requireAuth('member');

$projectManager = new ProjectManager($db, $app);
$user = $app->getCurrentUser();

// Get dashboard data with caching
$projects = $projectManager->getProjects();
$recentTasks = $app->getCachedData("recent_tasks_{$user['id']}", function() use ($db, $user) {
    $query = "SELECT t.*, p.name as project_name 
             FROM tasks t 
             JOIN projects p ON t.project_id = p.id 
             WHERE t.assigned_to = :user_id 
             ORDER BY t.created_at DESC 
             LIMIT 10";
    $stmt = $db->prepare($query);
    $stmt->bindParam(':user_id', $user['id']);
    $stmt->execute();
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}, 300);

// Get user statistics
$userStats = $app->getCachedData("user_stats_{$user['id']}", function() use ($db, $user) {
    $query = "SELECT 
             COUNT(DISTINCT t.project_id) as active_projects,
             COUNT(t.id) as assigned_tasks,
             COUNT(CASE WHEN t.status = 'done' THEN 1 END) as completed_tasks,
             SUM(te.hours) as total_hours_logged
             FROM tasks t
             LEFT JOIN time_entries te ON t.id = te.task_id AND te.user_id = :user_id
             WHERE t.assigned_to = :user_id";
    $stmt = $db->prepare($query);
    $stmt->bindParam(':user_id', $user['id']);
    $stmt->execute();
    return $stmt->fetch(PDO::FETCH_ASSOC);
}, 600);
?>
<!DOCTYPE html>
<html>
<head>
    <title>Project Management Dashboard</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        :root {
            --primary: #4361ee;
            --secondary: #3f37c9;
            --success: #4cc9f0;
            --danger: #f72585;
            --warning: #f8961e;
            --light: #f8f9fa;
            --dark: #212529;
        }
        
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f7fb; color: #333; line-height: 1.6; }
        
        .navbar { background: white; box-shadow: 0 2px 10px rgba(0,0,0,0.1); padding: 1rem 0; }
        .nav-container { max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center; padding: 0 2rem; }
        .nav-links a { margin-left: 2rem; text-decoration: none; color: var(--dark); font-weight: 500; }
        .nav-links a:hover { color: var(--primary); }
        
        .container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }
        
        .welcome-banner { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: white; padding: 3rem; border-radius: 15px; margin-bottom: 2rem; }
        .welcome-banner h1 { font-size: 2.5rem; margin-bottom: 0.5rem; }
        
        .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
        .stat-card { background: white; padding: 1.5rem; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
        .stat-number { font-size: 2rem; font-weight: bold; color: var(--primary); margin-bottom: 0.5rem; }
        .stat-label { color: #666; font-size: 0.9rem; }
        
        .content-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 2rem; }
        
        .card { background: white; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }
        .card-header { background: var(--light); padding: 1rem 1.5rem; border-bottom: 1px solid #eee; }
        .card-body { padding: 1.5rem; }
        
        .project-list { list-style: none; }
        .project-item { padding: 1rem; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
        .project-item:last-child { border-bottom: none; }
        .project-info h4 { margin-bottom: 0.25rem; }
        .project-meta { color: #666; font-size: 0.9rem; }
        .project-status { padding: 0.25rem 0.75rem; border-radius: 20px; font-size: 0.8rem; font-weight: 500; }
        .status-planning { background: #e9ecef; color: #495057; }
        .status-active { background: #d1ecf1; color: #0c5460; }
        .status-completed { background: #d4edda; color: #155724; }
        
        .task-list { list-style: none; }
        .task-item { padding: 0.75rem 0; border-bottom: 1px solid #eee; }
        .task-item:last-child { border-bottom: none; }
        .task-title { font-weight: 500; margin-bottom: 0.25rem; }
        .task-meta { color: #666; font-size: 0.8rem; }
        
        .btn { display: inline-block; padding: 0.75rem 1.5rem; background: var(--primary); color: white; text-decoration: none; border-radius: 5px; font-weight: 500; border: none; cursor: pointer; }
        .btn:hover { background: var(--secondary); }
        
        @media (max-width: 768px) {
            .content-grid { grid-template-columns: 1fr; }
            .nav-container { flex-direction: column; gap: 1rem; }
            .nav-links a { margin: 0 0.5rem; }
        }
    </style>
</head>
<body>
    <nav class="navbar">
        <div class="nav-container">
            <h2>ProjectManager SaaS</h2>
            <div class="nav-links">
                <a href="dashboard.php">Dashboard</a>
                <a href="projects.php">Projects</a>
                <a href="tasks.php">Tasks</a>
                <a href="profile.php">Profile</a>
                <a href="logout.php">Logout</a>
            </div>
        </div>
    </nav>
    
    <div class="container">
        <!-- Welcome Banner -->
        <div class="welcome-banner">
            <h1>Welcome back, <?= htmlspecialchars($user['name']) ?>!</h1>
            <p>Here's what's happening with your projects today.</p>
        </div>
        
        <!-- Statistics Grid -->
        <div class="stats-grid">
            <div class="stat-card">
                <div class="stat-number"><?= $userStats['active_projects'] ?? 0 ?></div>
                <div class="stat-label">Active Projects</div>
            </div>
            <div class="stat-card">
                <div class="stat-number"><?= $userStats['assigned_tasks'] ?? 0 ?></div>
                <div class="stat-label">Assigned Tasks</div>
            </div>
            <div class="stat-card">
                <div class="stat-number"><?= $userStats['completed_tasks'] ?? 0 ?></div>
                <div class="stat-label">Completed Tasks</div>
            </div>
            <div class="stat-card">
                <div class="stat-number"><?= number_format($userStats['total_hours_logged'] ?? 0, 1) ?></div>
                <div class="stat-label">Hours Logged</div>
            </div>
        </div>
        
        <!-- Main Content -->
        <div class="content-grid">
            <!-- Projects Section -->
            <div class="card">
                <div class="card-header">
                    <h3>Recent Projects</h3>
                </div>
                <div class="card-body">
                    <ul class="project-list">
                        <?php foreach(array_slice($projects, 0, 5) as $project): ?>
                        <li class="project-item">
                            <div class="project-info">
                                <h4><?= htmlspecialchars($project['name']) ?></h4>
                                <div class="project-meta">
                                    <?= $project['task_count'] ?> tasks • 
                                    <?= $project['completed_tasks'] ?> completed
                                </div>
                            </div>
                            <span class="project-status status-<?= $project['status'] ?>">
                                <?= ucfirst($project['status']) ?>
                            </span>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if(count($projects) > 5): ?>
                    <div style="text-align: center; margin-top: 1rem;">
                        <a href="projects.php" class="btn">View All Projects</a>
                    </div>
                    <?php endif; ?>
                </div>
            </div>
            
            <!-- Recent Tasks -->
            <div class="card">
                <div class="card-header">
                    <h3>Your Tasks</h3>
                </div>
                <div class="card-body">
                    <ul class="task-list">
                        <?php foreach($recentTasks as $task): ?>
                        <li class="task-item">
                            <div class="task-title"><?= htmlspecialchars($task['title']) ?></div>
                            <div class="task-meta">
                                <?= $task['project_name'] ?> • 
                                Due: <?= $task['due_date'] ? date('M j', strtotime($task['due_date'])) : 'No due date' ?>
                            </div>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                    <?php if(empty($recentTasks)): ?>
                    <p style="text-align: center; color: #666; padding: 1rem;">No tasks assigned to you.</p>
                    <?php endif; ?>
                </div>
            </div>
        </div>
    </div>
    
    <script>
        // Real-time updates with AJAX
        function refreshDashboard() {
            fetch('api/dashboard_stats.php')
                .then(response => response.json())
                .then(data => {
                    // Update stats dynamically
                    document.querySelectorAll('.stat-number')[0].textContent = data.active_projects;
                    document.querySelectorAll('.stat-number')[1].textContent = data.assigned_tasks;
                    document.querySelectorAll('.stat-number')[2].textContent = data.completed_tasks;
                    document.querySelectorAll('.stat-number')[3].textContent = data.total_hours.toFixed(1);
                })
                .catch(error => console.error('Error refreshing dashboard:', error));
        }
        
        // Refresh every 2 minutes
        setInterval(refreshDashboard, 120000);
        
        // Add smooth animations
        document.addEventListener('DOMContentLoaded', function() {
            const cards = document.querySelectorAll('.card, .stat-card');
            cards.forEach((card, index) => {
                card.style.opacity = '0';
                card.style.transform = 'translateY(20px)';
                card.style.transition = `all 0.5s ease ${index * 0.1}s`;
                
                setTimeout(() => {
                    card.style.opacity = '1';
                    card.style.transform = 'translateY(0)';
                }, 100);
            });
        });
    </script>
</body>
</html>

Key Enterprise Features Covered:

  1. Advanced Security: CSRF protection, rate limiting, input validation

  2. Performance Optimization: Caching, query optimization, lazy loading

  3. Multi-tenancy: SaaS architecture with tenant isolation

  4. Real-time Features: AJAX updates, WebSocket-ready structure

  5. Scalable Architecture: Modular design, service classes

  6. Professional UI: Responsive design, modern styling

Practice Exercises:

  1. Implement real-time notifications with WebSockets

  2. Add payment processing with Stripe integration

  3. Create an admin panel for tenant management

  4. Implement advanced reporting and analytics

  5. Add API rate limiting and documentation

  6. Set up automated testing with PHPUnit

Deployment & DevOps Preview:

In the next section, we'll cover:

  • Docker containerization

  • CI/CD pipelines with GitHub Actions

  • AWS/cloud deployment

  • Monitoring and logging

  • Database optimization and scaling

You've now built a professional-grade SaaS application with enterprise-level features! This represents the culmination of modern PHP development practices used in production environments.

What's Your Reaction?

like

dislike

love

funny

angry

sad

wow

MA Hussain Amjad Hussain: Architect of Truth in the Realm of Fact In the intricate tapestry of human experience, where shadow often meets light, Amjad Hussain stands as a dedicated chronicler of reality. His is a world built on the unshakeable foundations of fact, a pursuit he undertakes with the dual tools of a journalist's discerning eye and a writer's compelling narrative skill.