PHP Fundamentals
PHP Configuration & Best Practices
PHP Configuration & Best Practices
Proper PHP configuration and following best practices are crucial for building secure, performant, and maintainable applications. This lesson covers essential configuration settings and coding standards.
Understanding php.ini
The php.ini file contains configuration directives that control PHP behavior:
; Location of php.ini:
; - Linux: /etc/php/8.x/php.ini
; - Windows: C:\php\php.ini
; - macOS: /usr/local/etc/php/8.x/php.ini
; Find your php.ini location:
php --ini
; View all PHP settings:
php -i | grep "Configuration File"
Essential PHP.ini Settings
; === Error Handling ===
; Development environment
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
; Production environment
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
error_log = /var/log/php/error.log
; === Security Settings ===
expose_php = Off ; Hide PHP version
allow_url_fopen = Off ; Disable remote file access
allow_url_include = Off ; Disable remote file inclusion
disable_functions = exec,passthru,shell_exec,system,proc_open,popen
open_basedir = /var/www:/tmp ; Restrict file access
; === Performance Settings ===
max_execution_time = 30 ; Maximum execution time (seconds)
max_input_time = 60 ; Maximum input parsing time
memory_limit = 128M ; Maximum memory per script
post_max_size = 8M ; Maximum POST data size
upload_max_filesize = 2M ; Maximum upload file size
; === Session Settings ===
session.cookie_httponly = 1 ; Prevent JavaScript access to cookies
session.cookie_secure = 1 ; Only send cookies over HTTPS
session.use_strict_mode = 1 ; Reject uninitialized session IDs
session.gc_maxlifetime = 1440 ; Session garbage collection (24 min)
; === OPcache (Production) ===
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 10000
opcache.validate_timestamps = 0 ; Disable for production
Warning: Never set
display_errors = On in production! This can expose sensitive information to attackers.
Runtime Configuration with ini_set()
You can change some settings at runtime using ini_set():
<?php
// Change settings at runtime
ini_set('display_errors', 1);
ini_set('memory_limit', '256M');
ini_set('max_execution_time', 300);
ini_set('error_log', '/custom/path/errors.log');
// Get current setting value
echo ini_get('memory_limit'); // 256M
echo ini_get('max_execution_time'); // 300
// Check if a directive can be changed
$directives = [
'display_errors',
'memory_limit',
'max_execution_time'
];
foreach ($directives as $directive) {
$mode = ini_get_all($directive)[$directive]['access'];
echo "$directive can be changed: ";
echo ($mode & INI_USER) ? 'Yes' : 'No';
echo "\n";
}
// Environment-based configuration
if ($_SERVER['SERVER_NAME'] === 'localhost') {
// Development
ini_set('display_errors', 1);
error_reporting(E_ALL);
} else {
// Production
ini_set('display_errors', 0);
ini_set('log_errors', 1);
}
?>
Note: Not all php.ini directives can be changed with ini_set(). Some require changes in php.ini or .htaccess.
Security Best Practices
<?php
// 1. Input Validation
function validateEmail($email) {
return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
}
function validateAge($age) {
return filter_var($age, FILTER_VALIDATE_INT, [
'options' => ['min_range' => 0, 'max_range' => 150]
]) !== false;
}
// 2. Output Escaping
$userInput = "<script>alert('XSS')</script>";
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');
// 3. SQL Injection Prevention
// ❌ Never do this:
$sql = "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'";
// ✅ Always use prepared statements:
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$_POST['email']]);
// 4. Password Security
$password = $_POST['password'];
// Hash password
$hash = password_hash($password, PASSWORD_DEFAULT);
// Verify password
if (password_verify($password, $hash)) {
echo "Password correct";
}
// 5. CSRF Protection
session_start();
// Generate token
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
// In form:
// <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
// Verify token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
die('CSRF token validation failed');
}
// 6. Secure Session Management
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1); // Only for HTTPS
ini_set('session.use_strict_mode', 1);
session_start();
// Regenerate session ID after login
session_regenerate_id(true);
?>
Code Organization Best Practices
<?php
// 1. Use meaningful variable names
// ❌ Bad
$d = 86400;
$arr = [1, 2, 3];
// ✅ Good
$secondsPerDay = 86400;
$userIds = [1, 2, 3];
// 2. Use constants for fixed values
define('TAX_RATE', 0.15);
define('MAX_LOGIN_ATTEMPTS', 5);
// Or use const (better for classes)
const DATABASE_HOST = 'localhost';
const DATABASE_NAME = 'myapp';
// 3. One class per file
// File: User.php
class User {
private $id;
private $email;
public function __construct($id, $email) {
$this->id = $id;
$this->email = $email;
}
}
// 4. Proper namespacing
namespace App\Models;
class Product {
// Class implementation
}
// 5. Use type declarations (PHP 7+)
function addNumbers(int $a, int $b): int {
return $a + $b;
}
function getUser(int $id): ?User {
// Returns User or null
return $user ?? null;
}
// 6. DRY Principle (Don't Repeat Yourself)
// ❌ Bad - Repeated code
if ($user['role'] === 'admin') {
// Admin code
}
if ($user['role'] === 'admin') {
// Same check again
}
// ✅ Good - Single responsibility
function isAdmin($user): bool {
return $user['role'] === 'admin';
}
if (isAdmin($user)) {
// Admin code
}
?>
Error Handling Best Practices
<?php
// 1. Use exceptions for error handling
class ValidationException extends Exception {}
function registerUser($email, $password) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new ValidationException("Invalid email address");
}
if (strlen($password) < 8) {
throw new ValidationException("Password must be at least 8 characters");
}
// Registration logic
}
try {
registerUser($_POST['email'], $_POST['password']);
} catch (ValidationException $e) {
echo "Validation error: " . $e->getMessage();
} catch (Exception $e) {
error_log($e->getMessage());
echo "An error occurred. Please try again.";
}
// 2. Fail fast
function processPayment($amount) {
if ($amount <= 0) {
throw new InvalidArgumentException("Amount must be positive");
}
if (!isUserAuthenticated()) {
throw new Exception("User must be authenticated");
}
// Process payment
}
// 3. Log errors properly
function logError($message, $context = []) {
$timestamp = date('Y-m-d H:i:s');
$contextJson = json_encode($context);
error_log("[$timestamp] $message | Context: $contextJson");
}
try {
$result = riskyOperation();
} catch (Exception $e) {
logError("Operation failed", [
'exception' => get_class($e),
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
]);
}
?>
Performance Best Practices
<?php
// 1. Use OPcache in production
// Enable in php.ini:
// opcache.enable=1
// opcache.memory_consumption=128
// 2. Avoid repeated function calls
// ❌ Bad
for ($i = 0; $i < count($items); $i++) {
echo $items[$i];
}
// ✅ Good
$count = count($items);
for ($i = 0; $i < $count; $i++) {
echo $items[$i];
}
// 3. Use built-in functions
// ❌ Bad - Custom implementation
function arraySum($arr) {
$sum = 0;
foreach ($arr as $val) {
$sum += $val;
}
return $sum;
}
// ✅ Good - Built-in function
$sum = array_sum($arr);
// 4. Database query optimization
// ❌ Bad - N+1 query problem
$users = $db->query("SELECT * FROM users");
foreach ($users as $user) {
$orders = $db->query("SELECT * FROM orders WHERE user_id = " . $user['id']);
}
// ✅ Good - Single query with JOIN
$users = $db->query("
SELECT u.*, o.*
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
");
// 5. Use lazy loading
class User {
private $orders = null;
public function getOrders() {
if ($this->orders === null) {
$this->orders = $this->loadOrders();
}
return $this->orders;
}
}
// 6. Cache expensive operations
function getPopularProducts() {
$cacheKey = 'popular_products';
$cached = cache_get($cacheKey);
if ($cached !== false) {
return $cached;
}
$products = expensiveDatabaseQuery();
cache_set($cacheKey, $products, 3600); // Cache for 1 hour
return $products;
}
?>
Tip: Enable OPcache in production to cache compiled PHP code and significantly improve performance.
Dependency Management with Composer
# Install Composer
curl -sS https://getcomposer.org/installer | php
# Create composer.json
{
"require": {
"monolog/monolog": "^2.0",
"guzzlehttp/guzzle": "^7.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
# Install dependencies
composer install
# Update dependencies
composer update
# Autoload classes
require 'vendor/autoload.php';
<?php
// Using Composer autoloading
require __DIR__ . '/vendor/autoload.php';
// Use installed packages
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('app');
$log->pushHandler(new StreamHandler('app.log', Logger::WARNING));
$log->warning('This is a warning');
// Autoload your own classes
use App\Models\User;
use App\Controllers\UserController;
$user = new User();
$controller = new UserController();
?>
Environment Configuration
<?php
// .env file (never commit to git!)
// DB_HOST=localhost
// DB_NAME=myapp
// DB_USER=root
// DB_PASS=secret
// APP_ENV=production
// APP_DEBUG=false
// Load environment variables
function loadEnv($path) {
if (!file_exists($path)) {
throw new Exception(".env file not found");
}
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue; // Skip comments
}
list($name, $value) = explode('=', $line, 2);
$_ENV[trim($name)] = trim($value);
putenv(trim($name) . '=' . trim($value));
}
}
loadEnv(__DIR__ . '/.env');
// Access environment variables
$dbHost = $_ENV['DB_HOST'] ?? 'localhost';
$dbName = getenv('DB_NAME');
$isDebug = $_ENV['APP_DEBUG'] === 'true';
// Configuration based on environment
$config = [
'database' => [
'host' => $_ENV['DB_HOST'],
'name' => $_ENV['DB_NAME'],
'user' => $_ENV['DB_USER'],
'pass' => $_ENV['DB_PASS']
],
'app' => [
'env' => $_ENV['APP_ENV'],
'debug' => $_ENV['APP_DEBUG'] === 'true'
]
];
?>
Code Documentation
<?php
/**
* Calculate the total price of items in cart
*
* This function applies discounts and tax to calculate
* the final total price for all items in the shopping cart.
*
* @param array $items Array of cart items with price and quantity
* @param float $taxRate Tax rate as decimal (e.g., 0.15 for 15%)
* @param float $discount Discount amount to subtract from total
* @return float The final total price including tax and discount
* @throws InvalidArgumentException If tax rate is negative
*/
function calculateTotal(array $items, float $taxRate = 0.15, float $discount = 0): float {
if ($taxRate < 0) {
throw new InvalidArgumentException("Tax rate cannot be negative");
}
$subtotal = 0;
foreach ($items as $item) {
$subtotal += $item['price'] * $item['quantity'];
}
$total = $subtotal - $discount;
$total += $total * $taxRate;
return round($total, 2);
}
/**
* User model representing a registered user
*
* @property int $id User ID
* @property string $email User email address
* @property string $name User full name
*/
class User {
/** @var int User ID */
private $id;
/** @var string User email */
private $email;
/**
* Create a new user instance
*
* @param int $id User ID
* @param string $email User email
*/
public function __construct(int $id, string $email) {
$this->id = $id;
$this->email = $email;
}
}
?>
Testing Best Practices
<?php
// Install PHPUnit
// composer require --dev phpunit/phpunit
// Test file: tests/CalculatorTest.php
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase {
public function testAddition() {
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testDivisionByZero() {
$this->expectException(DivisionByZeroError::class);
$calculator = new Calculator();
$calculator->divide(10, 0);
}
public function testMultiplication() {
$calculator = new Calculator();
$this->assertEquals(6, $calculator->multiply(2, 3));
$this->assertEquals(0, $calculator->multiply(5, 0));
$this->assertEquals(-10, $calculator->multiply(5, -2));
}
}
// Run tests
// vendor/bin/phpunit tests
?>
Coding Standards (PSR-12)
<?php
// PSR-12 Coding Standard
// 1. Opening PHP tag and namespace
<?php
namespace App\Controllers;
// 2. Use statements
use App\Models\User;
use App\Services\EmailService;
// 3. Class declaration
class UserController
{
// 4. Properties
private $emailService;
protected $users = [];
// 5. Constructor
public function __construct(EmailService $emailService)
{
$this->emailService = $emailService;
}
// 6. Methods
public function createUser(string $email, string $name): User
{
// 7. Control structures
if (empty($email)) {
throw new InvalidArgumentException('Email is required');
}
// 8. Indentation: 4 spaces
$user = new User();
$user->setEmail($email);
$user->setName($name);
// 9. Method chaining
$user->setEmail($email)
->setName($name)
->setStatus('active');
return $user;
}
// 10. Array formatting
private function getConfig(): array
{
return [
'smtp_host' => 'localhost',
'smtp_port' => 587,
'from_email' => 'noreply@example.com',
];
}
}
?>
Exercise:
- Create a configuration class that loads settings from a .env file
- Implement a secure login system with password hashing and CSRF protection
- Write a reusable validator class with methods for common validations
- Create a simple logger class that writes to different files based on log level
- Build a database wrapper class using PDO with prepared statements
Production Deployment Checklist
Before deploying to production:
- ✓ Set
display_errors = Offin php.ini - ✓ Enable error logging:
log_errors = On - ✓ Set appropriate file permissions (644 for files, 755 for directories)
- ✓ Remove development tools and debug code
- ✓ Enable OPcache for better performance
- ✓ Use environment variables for sensitive data
- ✓ Implement CSRF and XSS protection
- ✓ Use HTTPS (SSL/TLS certificates)
- ✓ Set secure session cookie parameters
- ✓ Keep PHP and dependencies updated
- ✓ Implement rate limiting for APIs
- ✓ Set up automated backups
Common Mistakes to Avoid
<?php
// ❌ 1. Displaying errors in production
ini_set('display_errors', 1);
// ❌ 2. Not using prepared statements
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
// ❌ 3. Storing passwords in plain text
$password = $_POST['password'];
$sql = "INSERT INTO users (password) VALUES ('$password')";
// ❌ 4. Not validating user input
$email = $_POST['email'];
sendEmail($email);
// ❌ 5. Using deprecated functions
mysql_connect(); // Removed in PHP 7
ereg(); // Removed in PHP 7
// ✅ Correct approaches
ini_set('display_errors', 0);
ini_set('log_errors', 1);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_GET['id']]);
$hashedPassword = password_hash($_POST['password'], PASSWORD_DEFAULT);
if (filter_var($_POST['email'], FILTER_VALIDATE_EMAIL)) {
sendEmail($_POST['email']);
}
mysqli_connect(); // Use mysqli or PDO
preg_match(); // Use PCRE functions
?>
Remember: Security and performance should be considered from the beginning of your project, not added as an afterthought. Follow these best practices consistently to build robust PHP applications.