How to Build a Perfect Calculator with PHP: Step-by-Step Guide

0

Introduction

Building a calculator might seem like a beginner’s task, but creating a robust, user-friendly calculator application with proper error handling and security considerations is an excellent way to master PHP fundamentals. In this comprehensive guide, we’ll build a feature-rich calculator that handles basic operations, advanced functions, and provides a polished user experience.

Table of Contents

  1. Project Setup
  2. HTML Structure
  3. CSS Styling
  4. PHP Logic
  5. Advanced Features
  6. Security Considerations
  7. Testing and Deployment

1. Project Setup

File Structure

text

calculator/
├── index.php
├── style.css
├── calculator.js
└── README.md

Server Requirements

  • PHP 7.4 or higher
  • Web server (Apache/Nginx)
  • Browser with JavaScript enabled

2. HTML Structure

Create the index.php file with a clean, semantic structure:

php

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Advanced PHP Calculator</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="calculator-container">
        <h1>Advanced Calculator</h1>
        
        <form method="POST" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
            <div class="display">
                <input type="text" id="result" name="result" 
                       value="<?php echo isset($result) ? htmlspecialchars($result) : ''; ?>" 
                       readonly>
            </div>
            
            <div class="buttons">
                <!-- Memory Functions -->
                <button type="button" class="memory-btn" onclick="memoryRecall()">MR</button>
                <button type="button" class="memory-btn" onclick="memoryStore()">MS</button>
                <button type="button" class="memory-btn" onclick="memoryClear()">MC</button>
                
                <!-- Scientific Functions -->
                <button type="button" class="sci-btn" onclick="insertValue('sin(')">sin</button>
                <button type="button" class="sci-btn" onclick="insertValue('cos(')">cos</button>
                <button type="button" class="sci-btn" onclick="insertValue('tan(')">tan</button>
                <button type="button" class="sci-btn" onclick="insertValue('log(')">log</button>
                <button type="button" class="sci-btn" onclick="insertValue('ln(')">ln</button>
                <button type="button" class="sci-btn" onclick="insertValue('sqrt(')">√</button>
                
                <!-- Basic Operations -->
                <button type="button" onclick="clearDisplay()">C</button>
                <button type="button" onclick="deleteLast()">⌫</button>
                <button type="button" onclick="insertValue('%')">%</button>
                <button type="button" onclick="insertValue('/')">÷</button>
                
                <button type="button" onclick="insertValue('7')">7</button>
                <button type="button" onclick="insertValue('8')">8</button>
                <button type="button" onclick="insertValue('9')">9</button>
                <button type="button" onclick="insertValue('*')">×</button>
                
                <button type="button" onclick="insertValue('4')">4</button>
                <button type="button" onclick="insertValue('5')">5</button>
                <button type="button" onclick="insertValue('6')">6</button>
                <button type="button" onclick="insertValue('-')">−</button>
                
                <button type="button" onclick="insertValue('1')">1</button>
                <button type="button" onclick="insertValue('2')">2</button>
                <button type="button" onclick="insertValue('3')">3</button>
                <button type="button" onclick="insertValue('+')">+</button>
                
                <button type="button" onclick="insertValue('0')" class="zero">0</button>
                <button type="button" onclick="insertValue('.')">.</button>
                <button type="submit" name="calculate" class="equals">=</button>
            </div>
        </form>
        
        <div class="history">
            <h3>History</h3>
            <div id="history-list">
                <?php
                if (isset($_SESSION['history'])) {
                    foreach ($_SESSION['history'] as $entry) {
                        echo "<div class='history-item'>" . htmlspecialchars($entry) . "</div>";
                    }
                }
                ?>
            </div>
        </div>
    </div>
    
    <script src="calculator.js"></script>
</body>
</html>

3. CSS Styling

Create style.css for a modern, responsive design:

css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 20px;
}

.calculator-container {
    background: #2d3436;
    border-radius: 20px;
    padding: 30px;
    box-shadow: 0 20px 60px rgba(0,0,0,0.3);
    max-width: 500px;
    width: 100%;
}

h1 {
    color: #dfe6e9;
    text-align: center;
    font-size: 24px;
    margin-bottom: 20px;
    font-weight: 300;
    letter-spacing: 2px;
}

.display {
    background: #1a1a1a;
    border-radius: 10px;
    padding: 15px;
    margin-bottom: 20px;
}

.display input {
    width: 100%;
    background: transparent;
    border: none;
    color: #dfe6e9;
    font-size: 32px;
    text-align: right;
    padding: 5px;
    font-family: 'Courier New', monospace;
}

.display input:focus {
    outline: none;
}

.buttons {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 10px;
}

button {
    padding: 18px;
    font-size: 18px;
    border: none;
    border-radius: 10px;
    cursor: pointer;
    transition: all 0.2s ease;
    font-weight: 500;
}

button:hover {
    transform: scale(0.95);
    opacity: 0.8;
}

button:active {
    transform: scale(0.9);
}

.memory-btn {
    background: #636e72;
    color: white;
    font-size: 14px;
}

.sci-btn {
    background: #0984e3;
    color: white;
    font-size: 14px;
}

.zero {
    grid-column: span 2;
}

.equals {
    background: #00b894;
    color: white;
    font-size: 24px;
}

button:not(.memory-btn):not(.sci-btn):not(.equals) {
    background: #dfe6e9;
    color: #2d3436;
}

button:not(.memory-btn):not(.sci-btn):not(.equals):hover {
    background: #b2bec3;
}

.history {
    margin-top: 20px;
    background: #1a1a1a;
    border-radius: 10px;
    padding: 15px;
    max-height: 150px;
    overflow-y: auto;
}

.history h3 {
    color: #dfe6e9;
    font-size: 14px;
    font-weight: 300;
    margin-bottom: 10px;
    text-transform: uppercase;
    letter-spacing: 1px;
}

.history-item {
    color: #b2bec3;
    padding: 5px 0;
    border-bottom: 1px solid #2d3436;
    font-size: 14px;
    font-family: 'Courier New', monospace;
}

.history-item:last-child {
    border-bottom: none;
}

/* Scrollbar styling */
.history::-webkit-scrollbar {
    width: 5px;
}

.history::-webkit-scrollbar-track {
    background: #1a1a1a;
}

.history::-webkit-scrollbar-thumb {
    background: #636e72;
    border-radius: 5px;
}

/* Responsive Design */
@media (max-width: 480px) {
    .calculator-container {
        padding: 20px;
    }
    
    button {
        padding: 15px;
        font-size: 16px;
    }
    
    .display input {
        font-size: 28px;
    }
}

4. PHP Logic

Now, let’s implement the core PHP logic in index.php (add this at the top of your file):

php

<?php
session_start();

// Initialize history if not exists
if (!isset($_SESSION['history'])) {
    $_SESSION['history'] = [];
}

// Function to evaluate mathematical expressions safely
function calculate($expression) {
    // Remove all whitespace
    $expression = str_replace(' ', '', $expression);
    
    // Define allowed characters (numbers, operators, parentheses, dots)
    $allowed = '/^[0-9+\-*\/\(\)%\.]+$/';
    
    if (!preg_match($allowed, $expression)) {
        return "Error: Invalid characters";
    }
    
    // Handle percentage
    $expression = preg_replace('/(\d+)%/', '($1/100)', $expression);
    
    // Handle scientific functions
    $expression = preg_replace('/sin\(([^)]+)\)/', 'sin(deg2rad($1))', $expression);
    $expression = preg_replace('/cos\(([^)]+)\)/', 'cos(deg2rad($1))', $expression);
    $expression = preg_replace('/tan\(([^)]+)\)/', 'tan(deg2rad($1))', $expression);
    $expression = preg_replace('/log\(([^)]+)\)/', 'log10($1)', $expression);
    $expression = preg_replace('/ln\(([^)]+)\)/', 'log($1)', $expression);
    $expression = preg_replace('/sqrt\(([^)]+)\)/', 'sqrt($1)', $expression);
    
    try {
        // Evaluate the expression
        $result = eval("return $expression;");
        
        // Check if result is a number
        if (!is_numeric($result)) {
            return "Error: Invalid result";
        }
        
        // Round to prevent floating point errors
        if (is_float($result)) {
            $result = round($result, 10);
        }
        
        return $result;
    } catch (Exception $e) {
        return "Error: " . $e->getMessage();
    } catch (ParseError $e) {
        return "Error: Invalid expression";
    }
}

// Handle form submission
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST['calculate'])) {
    $expression = $_POST['result'] ?? '';
    
    if (!empty($expression)) {
        // Save expression to history
        $_SESSION['history'][] = $expression . " = " . calculate($expression);
        
        // Limit history to 20 entries
        if (count($_SESSION['history']) > 20) {
            array_shift($_SESSION['history']);
        }
        
        $result = calculate($expression);
    }
}

// Clear history if requested
if (isset($_GET['clear_history'])) {
    $_SESSION['history'] = [];
    header("Location: index.php");
    exit();
}

// Handle AJAX requests
if (isset($_POST['ajax']) && $_POST['ajax'] == '1') {
    $expression = $_POST['expression'] ?? '';
    $result = calculate($expression);
    echo json_encode(['result' => $result]);
    exit();
}
?>

5. Advanced Features

JavaScript Enhancement

Create calculator.js to handle client-side interactions:

javascript

// Global variables
let memory = 0;
let expression = '';
let isNewCalculation = true;

// Insert value into display
function insertValue(value) {
    const display = document.getElementById('result');
    
    if (isNewCalculation && !isNaN(value) && value !== '.') {
        display.value = value;
        isNewCalculation = false;
        return;
    }
    
    display.value += value;
    isNewCalculation = false;
}

// Clear display
function clearDisplay() {
    document.getElementById('result').value = '';
    isNewCalculation = true;
}

// Delete last character
function deleteLast() {
    const display = document.getElementById('result');
    display.value = display.value.slice(0, -1);
    if (display.value === '') {
        isNewCalculation = true;
    }
}

// Memory functions
function memoryStore() {
    const display = document.getElementById('result');
    memory = parseFloat(display.value) || 0;
}

function memoryRecall() {
    const display = document.getElementById('result');
    if (memory !== 0) {
        display.value = memory;
        isNewCalculation = false;
    }
}

function memoryClear() {
    memory = 0;
}

// Keyboard support
document.addEventListener('keydown', function(event) {
    const key = event.key;
    const display = document.getElementById('result');
    
    if (key >= '0' && key <= '9') {
        insertValue(key);
        event.preventDefault();
    } else if (key === '.') {
        insertValue('.');
        event.preventDefault();
    } else if (key === '+' || key === '-' || key === '*' || key === '/') {
        insertValue(key);
        event.preventDefault();
    } else if (key === 'Enter') {
        document.querySelector('[name="calculate"]').click();
        event.preventDefault();
    } else if (key === 'Backspace') {
        deleteLast();
        event.preventDefault();
    } else if (key === 'Escape') {
        clearDisplay();
        event.preventDefault();
    }
});

// Add validation before form submission
document.querySelector('form').addEventListener('submit', function(e) {
    const display = document.getElementById('result');
    if (display.value === '') {
        e.preventDefault();
        display.placeholder = 'Enter an expression';
        display.style.color = '#ff6b6b';
        setTimeout(() => {
            display.style.color = '#dfe6e9';
        }, 2000);
        return false;
    }
});

// Auto-calculate on equals button click (with AJAX)
document.querySelector('.equals').addEventListener('click', function(e) {
    const display = document.getElementById('result');
    const expression = display.value;
    
    if (expression.trim() === '') {
        e.preventDefault();
        return false;
    }
    
    // Try AJAX calculation
    fetch('index.php', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
        },
        body: 'ajax=1&expression=' + encodeURIComponent(expression)
    })
    .then(response => response.json())
    .then(data => {
        if (data.result) {
            display.value = data.result;
            isNewCalculation = true;
        }
    })
    .catch(error => {
        console.error('Error:', error);
        // Fallback to form submission
        document.querySelector('[name="calculate"]').click();
    });
    
    e.preventDefault();
});

// Tooltip for scientific functions
document.querySelectorAll('.sci-btn').forEach(button => {
    button.addEventListener('mouseover', function() {
        const tooltip = document.createElement('div');
        tooltip.className = 'tooltip';
        tooltip.textContent = this.textContent;
        this.appendChild(tooltip);
    });
    
    button.addEventListener('mouseout', function() {
        const tooltip = this.querySelector('.tooltip');
        if (tooltip) {
            tooltip.remove();
        }
    });
});

Advanced PHP Calculator Class

For better code organization, create a Calculator class:

php

<?php
class AdvancedCalculator {
    private $history = [];
    private $memory = 0;
    private $precision = 10;
    
    public function __construct() {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }
        $this->loadHistory();
    }
    
    private function loadHistory() {
        if (isset($_SESSION['calculator_history'])) {
            $this->history = $_SESSION['calculator_history'];
        }
    }
    
    private function saveHistory() {
        $_SESSION['calculator_history'] = $this->history;
    }
    
    public function evaluate($expression) {
        // Sanitize and validate
        $expression = $this->sanitizeExpression($expression);
        
        if (!$this->validateExpression($expression)) {
            return ['error' => 'Invalid expression', 'result' => null];
        }
        
        // Process special functions
        $expression = $this->processFunctions($expression);
        
        try {
            $result = eval("return $expression;");
            
            if (!is_numeric($result)) {
                return ['error' => 'Invalid result', 'result' => null];
            }
            
            $result = round($result, $this->precision);
            
            // Add to history
            $this->addToHistory($expression, $result);
            
            return ['error' => null, 'result' => $result];
            
        } catch (Exception $e) {
            return ['error' => 'Calculation error: ' . $e->getMessage(), 'result' => null];
        }
    }
    
    private function sanitizeExpression($expression) {
        // Remove whitespace
        $expression = str_replace(' ', '', $expression);
        return $expression;
    }
    
    private function validateExpression($expression) {
        // Only allow safe characters
        $pattern = '/^[0-9+\-*\/\(\)%\.sin cos tan log ln sqrt]+$/i';
        return preg_match($pattern, $expression) === 1;
    }
    
    private function processFunctions($expression) {
        // Convert degree to radian for trigonometric functions
        $expression = preg_replace('/sin\(([^)]+)\)/', 'sin(deg2rad($1))', $expression);
        $expression = preg_replace('/cos\(([^)]+)\)/', 'cos(deg2rad($1))', $expression);
        $expression = preg_replace('/tan\(([^)]+)\)/', 'tan(deg2rad($1))', $expression);
        
        // Handle logarithms
        $expression = preg_replace('/log\(([^)]+)\)/', 'log10($1)', $expression);
        $expression = preg_replace('/ln\(([^)]+)\)/', 'log($1)', $expression);
        
        // Handle square root
        $expression = preg_replace('/sqrt\(([^)]+)\)/', 'sqrt($1)', $expression);
        
        return $expression;
    }
    
    private function addToHistory($expression, $result) {
        $entry = $expression . ' = ' . $result;
        array_unshift($this->history, $entry);
        
        // Keep only last 50 entries
        if (count($this->history) > 50) {
            $this->history = array_slice($this->history, 0, 50);
        }
        
        $this->saveHistory();
    }
    
    public function getHistory() {
        return $this->history;
    }
    
    public function clearHistory() {
        $this->history = [];
        $this->saveHistory();
    }
    
    public function memoryStore($value) {
        $this->memory = $value;
    }
    
    public function memoryRecall() {
        return $this->memory;
    }
    
    public function memoryClear() {
        $this->memory = 0;
    }
}

6. Security Considerations

Input Validation and Sanitization

php

// Security functions
function sanitizeInput($input) {
    // Remove potentially dangerous characters
    $input = strip_tags($input);
    $input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
    return $input;
}

// SQL Injection prevention (if using database)
function prepareQuery($connection, $query, $params) {
    $stmt = $connection->prepare($query);
    if ($stmt === false) {
        throw new Exception('Database error: ' . $connection->error);
    }
    
    if (!empty($params)) {
        $types = str_repeat('s', count($params));
        $stmt->bind_param($types, ...$params);
    }
    
    return $stmt;
}

// CSRF protection
function generateCSRFToken() {
    if (empty($_SESSION['csrf_token'])) {
        $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf_token'];
}

function verifyCSRFToken($token) {
    return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}

Rate Limiting

php

// Rate limiting to prevent abuse
function checkRateLimit($userId, $maxRequests = 100, $timeWindow = 3600) {
    $key = "rate_limit_{$userId}";
    $current = time();
    
    if (!isset($_SESSION[$key])) {
        $_SESSION[$key] = ['count' => 1, 'first_request' => $current];
        return true;
    }
    
    $data = $_SESSION[$key];
    
    if ($current - $data['first_request'] > $timeWindow) {
        // Reset window
        $_SESSION[$key] = ['count' => 1, 'first_request' => $current];
        return true;
    }
    
    if ($data['count'] >= $maxRequests) {
        return false;
    }
    
    $_SESSION[$key]['count']++;
    return true;
}

7. Testing and Deployment

Test Cases

php

// Unit tests for Calculator class
class CalculatorTest {
    private $calculator;
    
    public function __construct() {
        $this->calculator = new AdvancedCalculator();
    }
    
    public function testBasicOperations() {
        $tests = [
            ['2+2', 4],
            ['10-5', 5],
            ['3*4', 12],
            ['15/3', 5],
            ['2+3*4', 14],
            ['(2+3)*4', 20],
        ];
        
        foreach ($tests as $test) {
            $result = $this->calculator->evaluate($test[0]);
            assert($result['result'] === $test[1], "Test failed: {$test[0]}");
        }
    }
    
    public function testScientificFunctions() {
        $tests = [
            ['sin(30)', 0.5],
            ['cos(60)', 0.5],
            ['tan(45)', 1],
            ['sqrt(16)', 4],
            ['log(100)', 2],
            ['ln(2.718281828)', 1],
        ];
        
        foreach ($tests as $test) {
            $result = $this->calculator->evaluate($test[0]);
            assert(abs($result['result'] - $test[1]) < 0.001, "Test failed: {$test[0]}");
        }
    }
    
    public function testErrorHandling() {
        $result = $this->calculator->evaluate('2/0');
        assert($result['error'] !== null, "Division by zero should return error");
        
        $result = $this->calculator->evaluate('invalid');
        assert($result['error'] !== null, "Invalid expression should return error");
    }
}

// Run tests
$test = new CalculatorTest();
$test->testBasicOperations();
$test->testScientificFunctions();
$test->testErrorHandling();
echo "All tests passed!";

Deployment Checklist

  1. Server Configuration
    • Enable session_start() in PHP
    • Set appropriate display_errors settings
    • Configure error logging
  2. Security Headers

php

header("X-Frame-Options: DENY");
header("X-XSS-Protection: 1; mode=block");
header("X-Content-Type-Options: nosniff");
header("Referrer-Policy: strict-origin-when-cross-origin");
header("Content-Security-Policy: default-src 'self'");
  1. Database Integration (Optional)

sql

CREATE TABLE calculator_history (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    expression TEXT,
    result VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_created_at (created_at)
);

CREATE TABLE calculator_users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password_hash VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Conclusion

You’ve now built a complete, secure, and feature-rich calculator application with PHP! This project demonstrates:

  • Full-stack development with PHP and JavaScript
  • Security best practices including input validation and sanitization
  • User experience with responsive design and keyboard support
  • Advanced features like memory functions and scientific operations
  • Code organization using OOP principles

Next Steps

  1. Add more scientific functions (factorial, power, etc.)
  2. Implement user authentication
  3. Create a REST API version
  4. Add graphing capabilities
  5. Implement unit testing with PHPUnit

Resources

Leave A Reply

Your email address will not be published.

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept