Lesson 2.5: Functions and Scope

Functions are like specialized tools in a workshop—each one designed to perform a specific task efficiently and reliably. Instead of writing the same code over and over throughout your program, you create functions that package common operations into reusable blocks. This approach saves enormous amounts of time, reduces errors, and makes your programs much easier to understand and maintain.

Think about tools in your kitchen. You have a can opener for opening cans, a whisk for mixing ingredients, and a timer for tracking cooking time. Each tool has a specific purpose, and you can use them whenever needed without having to rebuild them from scratch. Functions work exactly the same way in programming—they're specialized code tools that perform specific tasks whenever you call upon them.

Creating Your First Functions

A function in PHP starts with the function keyword, followed by a name you choose, parentheses for inputs (called parameters), and curly braces containing the code to execute. Even the simplest functions can dramatically improve your code organization.

<?php
// Basic function that doesn't need any inputs
function sayHello() {
    echo "Hello, welcome to our website!\n";
}

// Function that takes one input
function greetUser($name) {
    echo "Hello, $name! Great to see you today.\n";
}

// Function that takes multiple inputs
function calculateRectangleArea($length, $width) {
    $area = $length * $width;
    return $area;
}

// Function that formats currency
function formatPrice($amount) {
    return "$" . number_format($amount, 2);
}

// Using the functions
sayHello();
greetUser("Sarah");
greetUser("Mike");

$roomArea = calculateRectangleArea(12, 10);
echo "Room area: $roomArea square feet\n";

$productPrice = 29.99;
echo "Product price: " . formatPrice($productPrice) . "\n";

$discountedPrice = $productPrice * 0.8;
echo "Sale price: " . formatPrice($discountedPrice) . "\n";
?>

Functions make your code modular and reusable. Instead of writing currency formatting code every time you display a price, you create one formatPrice function and use it everywhere. If you later need to change how prices display, you only need to update the function once.

Functions with Return Values

Functions can send information back to the code that called them using the return statement. This lets you create functions that calculate values, process data, or make decisions and give you the results.

<?php
// Mathematical functions
function addNumbers($a, $b) {
    return $a + $b;
}

function multiplyNumbers($a, $b) {
    return $a * $b;
}

function calculateCircleArea($radius) {
    $pi = 3.14159;
    return $pi * $radius * $radius;
}

// Text processing functions
function createFullName($firstName, $lastName) {
    return $firstName . " " . $lastName;
}

function countWords($text) {
    $words = explode(" ", trim($text));
    return count($words);
}

// Validation functions
function isValidEmail($email) {
    return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}

function isAdult($age) {
    return $age >= 18;
}

// Using functions with return values
$sum = addNumbers(15, 25);
echo "15 + 25 = $sum\n";

$circleArea = calculateCircleArea(5);
echo "Circle area: " . number_format($circleArea, 2) . "\n";

$customerName = createFullName("John", "Smith");
echo "Customer: $customerName\n";

$message = "This is a sample message with several words";
$wordCount = countWords($message);
echo "Message has $wordCount words\n";

$userEmail = "[email protected]";
if (isValidEmail($userEmail)) {
    echo "Email address is valid\n";
} else {
    echo "Please enter a valid email address\n";
}

$userAge = 16;
if (isAdult($userAge)) {
    echo "User can access adult content\n";
} else {
    echo "User access is restricted\n";
}
?>

Return values make functions incredibly powerful because they can feed into other functions, be stored in variables, or used directly in calculations and decisions. This creates a building-block approach where simple functions combine to create complex functionality.

Default Parameters: Making Functions Flexible

Default parameters let you provide fallback values when someone doesn't supply all the inputs your function expects. This makes your functions more flexible and easier to use in different situations.

<?php
// Function with default parameters
function createWelcomeMessage($name, $title = "Customer", $timeOfDay = "day") {
    return "Good $timeOfDay, $title $name! Welcome to our store.";
}

// Function for calculating shipping costs
function calculateShipping($weight, $distance = 100, $method = "standard") {
    $baseCost = 5.00;
    $weightCost = $weight * 0.50;
    $distanceCost = $distance * 0.02;

    if ($method === "express") {
        $methodMultiplier = 2.0;
    } elseif ($method === "overnight") {
        $methodMultiplier = 3.0;
    } else {
        $methodMultiplier = 1.0; // standard shipping
    }

    return ($baseCost + $weightCost + $distanceCost) * $methodMultiplier;
}

// Function for formatting text display
function displayProduct($name, $price, $currency = "USD", $showTax = true) {
    $formattedPrice = number_format($price, 2);

    if ($currency === "USD") {
        $symbol = "$";
    } elseif ($currency === "EUR") {
        $symbol = "€";
    } else {
        $symbol = $currency . " ";
    }

    $display = "$name: $symbol$formattedPrice";

    if ($showTax) {
        $tax = $price * 0.08;
        $totalWithTax = $price + $tax;
        $display .= " (+ $symbol" . number_format($tax, 2) . " tax = $symbol" . number_format($totalWithTax, 2) . ")";
    }

    return $display;
}

// Using functions with different parameter combinations
echo createWelcomeMessage("Alice") . "\n";
echo createWelcomeMessage("Bob", "Dr.") . "\n";
echo createWelcomeMessage("Carol", "Ms.", "evening") . "\n";

echo "\nShipping calculations:\n";
echo "Standard 5lb package: $" . number_format(calculateShipping(5), 2) . "\n";
echo "Express 5lb to 200 miles: $" . number_format(calculateShipping(5, 200, "express"), 2) . "\n";
echo "Overnight 10lb to 500 miles: $" . number_format(calculateShipping(10, 500, "overnight"), 2) . "\n";

echo "\nProduct displays:\n";
echo displayProduct("Laptop", 899.99) . "\n";
echo displayProduct("Book", 24.99, "USD", false) . "\n";
echo displayProduct("Headphones", 79.99, "EUR") . "\n";
?>

Default parameters must come after required parameters in your function definition. You can't have a required parameter after an optional one, so organize them from most important to least important.

Variable Scope: Where Variables Live

Understanding scope is crucial for writing reliable functions. Scope determines where in your code a variable can be accessed and modified. It's like understanding which rooms in a house you can access certain items from.

<?php
// Global variables - accessible throughout the entire script
$siteName = "My Online Store";
$currentUser = "guest";
$shoppingCartTotal = 0;

function displayHeader() {
    // To use global variables inside functions, you need the 'global' keyword
    global $siteName, $currentUser;

    echo "=== $siteName ===\n";
    echo "Logged in as: $currentUser\n";
    echo "==================\n";
}

function calculateOrderTotal($items) {
    // Local variables - only exist inside this function
    $subtotal = 0;
    $taxRate = 0.08;

    foreach ($items as $item) {
        $itemTotal = $item['price'] * $item['quantity'];
        $subtotal += $itemTotal;
    }

    $tax = $subtotal * $taxRate;
    $total = $subtotal + $tax;

    // These variables only exist inside this function
    return [
        'subtotal' => $subtotal,
        'tax' => $tax,
        'total' => $total
    ];
}

function updateUserStatus($username) {
    global $currentUser;

    // Change the global variable
    $currentUser = $username;

    // Local variable with same name as global - doesn't affect global
    $siteName = "Local Site Name"; // This doesn't change the global $siteName

    echo "User status updated to: $currentUser\n";
    echo "Local site name: $siteName\n";
}

// Using the functions
displayHeader();

$orderItems = [
    ['name' => 'T-shirt', 'price' => 19.99, 'quantity' => 2],
    ['name' => 'Jeans', 'price' => 49.99, 'quantity' => 1]
];

$orderSummary = calculateOrderTotal($orderItems);
echo "Order subtotal: $" . number_format($orderSummary['subtotal'], 2) . "\n";
echo "Tax: $" . number_format($orderSummary['tax'], 2) . "\n";
echo "Total: $" . number_format($orderSummary['total'], 2) . "\n\n";

updateUserStatus("alice_user");
echo "Global site name is still: $siteName\n";

displayHeader(); // Shows the updated user name

// Trying to access local variables outside their function causes errors
// echo $subtotal; // This would cause an error because $subtotal only exists inside calculateOrderTotal
?>

Global variables are accessible everywhere but should be used sparingly because they can make your code harder to understand and debug. Local variables are safer because they only exist where you expect them to, preventing accidental modifications from other parts of your code.

Static Variables: Remembering Between Calls

Static variables retain their values between function calls, making them perfect for counters, caches, or maintaining state information.

<?php
// Function with static counter
function generateOrderNumber() {
    static $orderCounter = 1000; // Initialized only once

    $orderCounter++;
    return "ORD-" . $orderCounter;
}

// Function with static cache for expensive calculations
function calculateFactorial($number) {
    static $cache = []; // Remembers previous calculations

    if ($number < 0) {
        return "Error: Factorial undefined for negative numbers";
    }

    if ($number <= 1) {
        return 1;
    }

    // Check if we've already calculated this
    if (isset($cache[$number])) {
        echo "(Using cached result for $number!) ";
        return $cache[$number];
    }

    // Calculate and store in cache
    $result = $number * calculateFactorial($number - 1);
    $cache[$number] = $result;

    return $result;
}

// Function that tracks usage statistics
function logPageView($page) {
    static $pageViews = [];

    if (!isset($pageViews[$page])) {
        $pageViews[$page] = 0;
    }

    $pageViews[$page]++;

    echo "Page '$page' has been viewed {$pageViews[$page]} time(s)\n";

    return $pageViews;
}

// Function with static initialization
function getSessionId() {
    static $sessionId = null;

    // Generate session ID only once
    if ($sessionId === null) {
        $sessionId = "sess_" . uniqid();
        echo "Generated new session ID: $sessionId\n";
    }

    return $sessionId;
}

// Testing static variables
echo "Order numbers:\n";
for ($i = 0; $i < 5; $i++) {
    echo generateOrderNumber() . "\n";
}

echo "\nFactorial calculations:\n";
echo "5! = " . calculateFactorial(5) . "\n";
echo "6! = " . calculateFactorial(6) . "\n";
echo "5! = " . calculateFactorial(5) . "\n"; // Should use cached result

echo "\nPage view tracking:\n";
logPageView("home");
logPageView("products");
logPageView("home");
logPageView("contact");
logPageView("home");

echo "\nSession ID calls:\n";
echo "First call: " . getSessionId() . "\n";
echo "Second call: " . getSessionId() . "\n";
echo "Third call: " . getSessionId() . "\n";
?>

Static variables are initialized only once, the first time the function runs. After that, they keep their values between function calls, making them useful for maintaining state without using global variables.

Anonymous Functions: Functions Without Names

Anonymous functions (also called closures) are functions that don't have names. They're useful for short, one-time operations or when you need to pass a function as a parameter to another function.

<?php
// Basic anonymous function stored in a variable
$greetCustomer = function($name, $type = "regular") {
    if ($type === "premium") {
        return "Welcome back, valued customer $name!";
    } else {
        return "Hello, $name! Thanks for visiting.";
    }
};

// Anonymous function with 'use' clause to access outside variables
$taxRate = 0.08;
$calculateTotalPrice = function($price) use ($taxRate) {
    $tax = $price * $taxRate;
    return $price + $tax;
};

// Array of anonymous functions for different operations
$mathOperations = [
    'square' => function($x) { return $x * $x; },
    'cube' => function($x) { return $x * $x * $x; },
    'double' => function($x) { return $x * 2; },
    'half' => function($x) { return $x / 2; }
];

// Function that accepts another function as a parameter
function processNumbers($numbers, $operation) {
    $results = [];
    foreach ($numbers as $number) {
        $results[] = $operation($number);
    }
    return $results;
}

// Function that returns a function (function factory)
function createDiscountCalculator($discountPercent) {
    return function($price) use ($discountPercent) {
        $discount = $price * ($discountPercent / 100);
        return $price - $discount;
    };
}

// Using anonymous functions
echo $greetCustomer("John") . "\n";
echo $greetCustomer("Sarah", "premium") . "\n";

echo "\nPrice with tax:\n";
$basePrice = 29.99;
$finalPrice = $calculateTotalPrice($basePrice);
echo "Base price: $" . number_format($basePrice, 2) . "\n";
echo "With tax: $" . number_format($finalPrice, 2) . "\n";

echo "\nMath operations on [2, 4, 6]:\n";
$numbers = [2, 4, 6];

foreach ($mathOperations as $operation => $function) {
    $results = processNumbers($numbers, $function);
    echo ucfirst($operation) . ": " . implode(", ", $results) . "\n";
}

echo "\nDiscount calculators:\n";
$tenPercentOff = createDiscountCalculator(10);
$twentyPercentOff = createDiscountCalculator(20);

$originalPrice = 100;
echo "Original price: $" . number_format($originalPrice, 2) . "\n";
echo "10% off: $" . number_format($tenPercentOff($originalPrice), 2) . "\n";
echo "20% off: $" . number_format($twentyPercentOff($originalPrice), 2) . "\n";

// Using anonymous functions for filtering
$products = [
    ['name' => 'Laptop', 'price' => 899, 'category' => 'electronics'],
    ['name' => 'T-shirt', 'price' => 25, 'category' => 'clothing'],
    ['name' => 'Phone', 'price' => 599, 'category' => 'electronics'],
    ['name' => 'Jeans', 'price' => 75, 'category' => 'clothing']
];

$expensiveItems = array_filter($products, function($product) {
    return $product['price'] > 100;
});

echo "\nExpensive items (over $100):\n";
foreach ($expensiveItems as $item) {
    echo $item['name'] . " - $" . $item['price'] . "\n";
}
?>

Anonymous functions are particularly useful for callback operations, where you need to pass a small piece of functionality to another function. They keep related code close together and avoid cluttering your namespace with single-use function names.

Error Handling in Functions

Good functions anticipate problems and handle them gracefully, providing clear feedback when things go wrong.

<?php
// Function with input validation
function divideNumbers($a, $b) {
    // Check if inputs are numbers
    if (!is_numeric($a) || !is_numeric($b)) {
        return "Error: Both inputs must be numbers";
    }

    // Check for division by zero
    if ($b == 0) {
        return "Error: Cannot divide by zero";
    }

    return $a / $b;
}

// Function that returns success/error information
function createUserAccount($username, $email, $password) {
    $errors = [];

    // Validate username
    if (empty($username)) {
        $errors[] = "Username is required";
    } elseif (strlen($username) < 3) {
        $errors[] = "Username must be at least 3 characters";
    }

    // Validate email
    if (empty($email)) {
        $errors[] = "Email is required";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $errors[] = "Email format is invalid";
    }

    // Validate password
    if (empty($password)) {
        $errors[] = "Password is required";
    } elseif (strlen($password) < 6) {
        $errors[] = "Password must be at least 6 characters";
    }

    // Return result
    if (empty($errors)) {
        return [
            'success' => true,
            'message' => "Account created successfully for $username",
            'user_id' => rand(1000, 9999) // Simulated user ID
        ];
    } else {
        return [
            'success' => false,
            'errors' => $errors
        ];
    }
}

// Function with range checking
function calculateSquareRoot($number) {
    if (!is_numeric($number)) {
        return ['success' => false, 'error' => 'Input must be a number'];
    }

    if ($number < 0) {
        return ['success' => false, 'error' => 'Cannot calculate square root of negative number'];
    }

    return ['success' => true, 'result' => sqrt($number)];
}

// Function with file operation safety
function readConfigFile($filename) {
    if (empty($filename)) {
        return ['success' => false, 'error' => 'Filename cannot be empty'];
    }

    if (!file_exists($filename)) {
        return ['success' => false, 'error' => "File '$filename' does not exist"];
    }

    $content = file_get_contents($filename);

    if ($content === false) {
        return ['success' => false, 'error' => "Could not read file '$filename'"];
    }

    return ['success' => true, 'content' => $content];
}

// Testing error handling
echo "Division tests:\n";
echo "15 ÷ 3 = " . divideNumbers(15, 3) . "\n";
echo "10 ÷ 0 = " . divideNumbers(10, 0) . "\n";
echo "abc ÷ 5 = " . divideNumbers("abc", 5) . "\n";

echo "\nUser account creation:\n";
$result1 = createUserAccount("jo", "invalid-email", "123");
if ($result1['success']) {
    echo $result1['message'] . "\n";
} else {
    echo "Account creation failed:\n";
    foreach ($result1['errors'] as $error) {
        echo "- $error\n";
    }
}

$result2 = createUserAccount("alice", "[email protected]", "securepass123");
if ($result2['success']) {
    echo $result2['message'] . " (ID: " . $result2['user_id'] . ")\n";
}

echo "\nSquare root calculations:\n";
$sqrtTest1 = calculateSquareRoot(16);
if ($sqrtTest1['success']) {
    echo "√16 = " . $sqrtTest1['result'] . "\n";
} else {
    echo "Error: " . $sqrtTest1['error'] . "\n";
}

$sqrtTest2 = calculateSquareRoot(-4);
if ($sqrtTest2['success']) {
    echo "√-4 = " . $sqrtTest2['result'] . "\n";
} else {
    echo "Error: " . $sqrtTest2['error'] . "\n";
}

echo "\nFile reading test:\n";
$fileTest = readConfigFile("nonexistent.txt");
if ($fileTest['success']) {
    echo "File content: " . substr($fileTest['content'], 0, 50) . "...\n";
} else {
    echo "Error: " . $fileTest['error'] . "\n";
}
?>

Well-designed functions validate their inputs, handle edge cases, and provide clear feedback about what went wrong. This approach makes your applications more robust and user-friendly.

Building a Complete Function Library

Let's combine everything we've learned to create a comprehensive function library that demonstrates all the concepts from this lesson.

<?php
// E-commerce function library

// Product management functions
function createProduct($name, $price, $category = "general", $stock = 0) {
    static $productIdCounter = 1000;

    $errors = [];

    if (empty($name)) {
        $errors[] = "Product name is required";
    }

    if (!is_numeric($price) || $price < 0) {
        $errors[] = "Price must be a positive number";
    }

    if (!is_numeric($stock) || $stock < 0) {
        $errors[] = "Stock must be a positive number";
    }

    if (!empty($errors)) {
        return ['success' => false, 'errors' => $errors];
    }

    $productIdCounter++;

    return [
        'success' => true,
        'product' => [
            'id' => $productIdCounter,
            'name' => $name,
            'price' => $price,
            'category' => $category,
            'stock' => $stock
        ]
    ];
}

function formatProductDisplay($product, $currency = "USD", $showStock = true) {
    $symbol = ($currency === "USD") ? "$" : "€";
    $display = $product['name'] . " - " . $symbol . number_format($product['price'], 2);

    if ($showStock) {
        $stockStatus = ($product['stock'] > 0) ? "{$product['stock']} in stock" : "Out of stock";
        $display .= " ($stockStatus)";
    }

    return $display;
}

// Shopping cart functions
function calculateCartTotal($items, $taxRate = 0.08, $shippingCost = 5.99) {
    static $calculations = 0;
    $calculations++;

    echo "Cart calculation #$calculations\n";

    $subtotal = 0;

    foreach ($items as $item) {
        if (!isset($item['price'], $item['quantity'])) {
            continue; // Skip invalid items
        }

        $itemTotal = $item['price'] * $item['quantity'];
        $subtotal += $itemTotal;
    }

    $tax = $subtotal * $taxRate;
    $total = $subtotal + $tax + $shippingCost;

    return [
        'subtotal' => $subtotal,
        'tax' => $tax,
        'shipping' => $shippingCost,
        'total' => $total
    ];
}

function applyDiscount($amount, $discountPercent) {
    if (!is_numeric($amount) || !is_numeric($discountPercent)) {
        return $amount; // Return original if invalid input
    }

    if ($discountPercent < 0 || $discountPercent > 100) {
        return $amount; // Invalid discount percentage
    }

    $discount = $amount * ($discountPercent / 100);
    return $amount - $discount;
}

// User management functions
global $currentUser;
$currentUser = null;

function loginUser($username, $password) {
    global $currentUser;

    // Simplified authentication (in real apps, check against database)
    $validUsers = [
        'admin' => 'admin123',
        'user1' => 'password1',
        'user2' => 'password2'
    ];

    if (isset($validUsers[$username]) && $validUsers[$username] === $password) {
        $currentUser = $username;
        return ['success' => true, 'message' => "Welcome, $username!"];
    } else {
        return ['success' => false, 'message' => 'Invalid username or password'];
    }
}

function getCurrentUser() {
    global $currentUser;
    return $currentUser ?? 'guest';
}

function logoutUser() {
    global $currentUser;
    $currentUser = null;
    return "Logged out successfully";
}

// Utility functions with anonymous functions
$priceFilters = [
    'under_25' => function($product) { return $product['price'] < 25; },
    'under_50' => function($product) { return $product['price'] < 50; },
    'under_100' => function($product) { return $product['price'] < 100; },
    'expensive' => function($product) { return $product['price'] >= 100; }
];

function filterProducts($products, $filterName) {
    global $priceFilters;

    if (!isset($priceFilters[$filterName])) {
        return $products; // Return all if filter doesn't exist
    }

    return array_filter($products, $priceFilters[$filterName]);
}

// Demonstration of the complete system
echo "=== E-commerce Function Library Demo ===\n\n";

// Create some products
$laptop = createProduct("Gaming Laptop", 899.99, "electronics", 5);
$mouse = createProduct("Wireless Mouse", 24.99, "electronics", 15);
$shirt = createProduct("Cotton T-Shirt", 19.99, "clothing", 25);

if ($laptop['success']) {
    echo "Created: " . formatProductDisplay($laptop['product']) . "\n";
}
if ($mouse['success']) {
    echo "Created: " . formatProductDisplay($mouse['product']) . "\n";
}
if ($shirt['success']) {
    echo "Created: " . formatProductDisplay($shirt['product']) . "\n";
}

// User login
echo "\n=== User Login ===\n";
$loginResult = loginUser("user1", "password1");
echo $loginResult['message'] . "\n";
echo "Current user: " . getCurrentUser() . "\n";

// Shopping cart calculation
echo "\n=== Shopping Cart ===\n";
$cartItems = [
    ['name' => 'Gaming Laptop', 'price' => 899.99, 'quantity' => 1],
    ['name' => 'Wireless Mouse', 'price' => 24.99, 'quantity' => 2]
];

$cartSummary = calculateCartTotal($cartItems);
echo "Subtotal: $" . number_format($cartSummary['subtotal'], 2) . "\n";
echo "Tax: $" . number_format($cartSummary['tax'], 2) . "\n";
echo "Shipping: $" . number_format($cartSummary['shipping'], 2) . "\n";
echo "Total: $" . number_format($cartSummary['total'], 2) . "\n";

// Apply discount
$discountedTotal = applyDiscount($cartSummary['total'], 10);
echo "After 10% discount: $" . number_format($discountedTotal, 2) . "\n";

// Product filtering
echo "\n=== Product Filtering ===\n";
$allProducts = [$laptop['product'], $mouse['product'], $shirt['product']];
$cheapProducts = filterProducts($allProducts, 'under_50');

echo "Products under $50:\n";
foreach ($cheapProducts as $product) {
    echo "- " . formatProductDisplay($product) . "\n";
}

// Logout
echo "\n" . logoutUser() . "\n";
echo "Current user: " . getCurrentUser() . "\n";
?>

Key Takeaways

Functions are fundamental building blocks that transform scattered code into organized, reusable components. They encapsulate specific functionality, reduce code duplication, and make your programs easier to understand, test, and maintain.

Understanding scope ensures your variables behave predictably—global variables are accessible everywhere but should be used sparingly, while local variables provide safety by staying contained within their functions. Static variables enable state persistence between function calls without the risks of global variables.

Default parameters make functions flexible and user-friendly, while return values enable functions to communicate results back to calling code. Anonymous functions provide powerful callback capabilities for dynamic functionality.

Error handling in functions creates robust applications that respond gracefully to unexpected inputs and edge cases. Always validate inputs, handle special cases, and provide clear feedback about what went wrong.

Practice breaking complex problems into smaller functions, each with a single clear purpose. Choose descriptive function names, organize parameters logically, and document your functions with comments explaining their purpose and expected inputs. Remember that clean, well-structured functions are the foundation of maintainable, professional code.

As you continue developing with PHP, functions will become your primary tool for organizing code and building sophisticated applications. Master these concepts, and you'll have the skills to tackle increasingly complex programming challenges with confidence.