PHP
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Object-Oriented PHP
-
Section 5: Working with Data
-
Section 6: Modern PHP (PHP 8)
-
Section 7: Working with Files and Networking
-
Section 8: Common PHP Frameworks Overview
-
Section 9: Tooling and Ecosystem
-
Section 10: Practical Projects
-
Section 11: Interview Practice
-
Section 12: More Standard Library
-
Section 13: Data Structures and Algorithms in PHP
-
Section 14: More Practice Exercises
-
Section 15: More Security and Best Practices
-
Section 16: More Testing and Tooling
-
Section 17: WordPress-Style CMS Concepts
-
Section 18: Advanced OOP Practice
-
Section 19: More Web Fundamentals
-
Section 20: Database Practice
-
Section 21: PHP Manual: Array Functions
-
Section 22: PHP Manual: Date and Calendar Functions
-
Section 23: PHP Manual: Filesystem and Directory Functions
-
Section 24: PHP Manual: Filter and Var Handling
-
Section 25: PHP Manual: Math Functions
-
Section 26: PHP Manual: JSON and XML
-
Section 27: PHP Manual: Network and Stream Functions
-
Section 28: PHP Manual: Error and Exception Handling
-
Section 29: PHP Manual: Output Control and Misc
-
Section 30: PHP Manual: FTP, Zip, and Mail
-
Section 31: Modern PHP Frameworks Deep Dive
-
Section 32: PHP Design Patterns
-
Section 33: More Practice Exercises
-
Section 34: PHP Performance and Deployment
-
Section 35: More Interview Practice
-
Section 36: More PHP Standard Library
-
Section 37: PHP Concurrency and Async
-
Section 38: More Web Development Practice
-
Section 39: PHP Testing Deep Dive
-
Section 40: Composer and Package Development
-
Section 41: PHP Security Deep Dive
-
Section 42: More Practical Projects
-
Section 43: Legacy PHP Maintenance
-
Section 44: More Algorithm Practice
-
Section 45: Final Practice and Review
-
Section 46: PHP for E-Commerce Patterns
-
Section 47: PHP API Design Deep Dive
-
Section 48: PHP Caching Strategies
-
Section 49: PHP Queue and Background Jobs
-
Section 50: PHP Multi-Tenancy Patterns
-
Section 51: PHP Real-Time Features
-
Section 52: PHP CMS and Content Modeling
-
Section 53: PHP Internationalization
-
Section 54: More Framework-Specific Practice
-
Section 55: PHP Legacy Code Refactoring
-
Section 56: More Practice Projects Round 2
-
Section 57: PHP Command-Line Applications
-
Section 58: PHP and Microservices
-
Section 59: More Interview and Review Round 2
212: Scoped Queries for Multi-Tenant Data
In a multi-tenant application, the biggest fear you'll ever have is "data leakage." That's the moment a user from Company A refreshes their page and suddenly sees an invoice belonging to Company B. It's a catastrophic failure of trust. We prevent this by "scoping" every single query to a specific tenant_id.
I want to show you how I handle this. We're going to build a small part of a project management tool where different agencies (our tenants) store their clients. If we just query by a client ID, we're asking for trouble. We need to ensure the query is always bound to the current agency.
Defining the Tenant Context
First, we need a way to know who the current tenant is. In a real app, you'd get this from a session or a JWT token. For this example, I'll just use a simple TenantContext class to hold the state. It keeps our code clean and prevents us from passing $tenantId into every single function argument, which gets tedious fast.
class TenantContext {
public static ?int $tenantId = null;
public static function setTenantId(int $id): void {
self::$tenantId = $id;
}
public static function getTenantId(): int {
if (self::$tenantId === null) {
throw new Exception("Tenant context not initialized.");
}
return self::$tenantId;
}
}
The Danger of Manual Scoping
Now, let's look at how we actually fetch data. The most intuitive way is to just add a WHERE clause to your SQL. I'll start with a simple function to fetch a client's details.
function getClient(PDO $pdo, int $clientId) {
$stmt = $pdo->prepare("SELECT * FROM clients WHERE id = ? AND tenant_id = ?");
$stmt->execute([$clientId, TenantContext::getTenantId()]);
return $stmt->fetch();
}
This looks fine, right? But here is where I usually trip up when I'm rushing—and where you likely will too. I'll write a function to update the client's name, and in my head, I'm thinking, "I already have the primary key, I don't need the tenant ID for the update."
// WARNING: This is the mistake I just made
function updateClientName(PDO $pdo, int $clientId, string $newName) {
$stmt = $pdo->prepare("UPDATE clients SET name = ? WHERE id = ?");
$stmt->execute([$newName, $clientId]);
}
Wait. Stop. If a malicious user from Tenant A knows (or guesses) the clientId of a client belonging to Tenant B, they can send a request to this function and change Tenant B's data. This is a classic IDOR (Insecure Direct Object Reference) vulnerability. I forgot to scope the UPDATE query.
Building a Robust Scoping Helper
To stop myself from making this mistake, I prefer to abstract the "where" clause. Instead of writing the SQL manually every time, I'll create a small helper that forces the tenant scope into the query. While full ORMs like Eloquent do this with "Global Scopes," you can implement a simplified version yourself to keep your queries safe.
class ClientRepository {
public function __construct(private PDO $pdo) {}
private function applyTenantScope(string $sql): string {
// Very simple implementation: append the tenant check
// In a real app, you'd use a more sophisticated query builder
if (str_contains($sql, 'WHERE')) {
return $sql . " AND tenant_id = " . TenantContext::getTenantId();
}
return $sql . " WHERE tenant_id = " . TenantContext::getTenantId();
}
public function updateName(int $clientId, string $newName) {
$sql = "UPDATE clients SET name = ? WHERE id = ?";
$scopedSql = $this->applyTenantScope($sql);
$stmt = $this->pdo->prepare($scopedSql);
// Note: We add the tenant ID to the parameters because
// applyTenantScope just appended the text for this simple example
$stmt->execute([$newName, $clientId]);
}
}
By routing my updates through a repository and a scoping method, I'm creating a "safety net." I no longer have to remember to add the tenant_id check every single time; the architecture does it for me. It's much harder to accidentally leak data when the default behavior is to restrict it.
📋 Practical Task
Exercise: Implementing Scoped Deletion for Invoices
You are working on a multi-tenant billing system. You have an invoices table with columns id, amount, and tenant_id. Currently, the system has a dangerous deleteInvoice($pdo, $invoiceId) function that only filters by the primary ID.
Your Task: Rewrite the deleteInvoice function to ensure that an invoice can only be deleted if it belongs to the current tenant. You must use the TenantContext::getTenantId() method to retrieve the current tenant's ID and include it in your SQL WHERE clause to prevent cross-tenant data deletion.
There are no comments for now.