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
210: Database-per-Tenant vs Shared Database Approaches
A few years ago, I was consulting for a developer who had built a promising SaaS for independent medical clinics. He'd gone with the simplest path: one giant database where every single table—patients, appointments, billing—had a clinic_id column. It worked beautifully until the day he accidentally pushed a bug to production that omitted a WHERE clause on a critical reporting query. For about forty minutes, Clinic A was able to see the patient lists and billing totals for Clinic B. In the medical world, that's not just a bug; it's a catastrophic HIPAA violation. He spent the next three days in a panic, realizing that while his architecture was easy to build, a single typo could leak every piece of data in the entire system.
The Simplicity and Risk of the Tenant ID
When you're starting out, the shared database approach (often called multi-tenancy via discriminator columns) is the path of least resistance. You have one database, one set of migrations, and one connection string in your .env file. To keep data separate, you simply tag every row with a tenant_id. When a user logs in, you store their tenant ID in the session and append WHERE tenant_id = ? to every single query you write.
From a developer's perspective, this is a dream for maintenance. If you need to add a "middle name" column to your users table, you run one migration, and every single client on your platform is updated instantly. However, as my friend discovered, the "blast radius" is huge. If you forget that WHERE clause—or if a junior developer joins the team and doesn't realize the convention—you've got a data leak. You're also dealing with "noisy neighbor" syndrome: if one massive client runs a resource-heavy report that locks a table, every other client on your platform feels the lag.
Hard Isolation and the Migration Nightmare
The alternative is the database-per-tenant approach. Here, every time a new client signs up, your PHP code triggers a script that creates a brand new database (e.g., tenant_123_db) and runs the full schema setup. Your application determines which database to connect to based on the request—usually by looking at the subdomain (like client1.myapp.com) or a custom header.
// A simplified conceptual look at switching connections
$tenantId = $request->getSubdomain();
$dbConfig = $mainDb->query("SELECT db_name, db_user, db_pass FROM tenants WHERE id = ?", [$tenantId]);
$pdo = new PDO("mysql:host=localhost;dbname=" . $dbConfig['db_name'], $dbConfig['db_user'], $dbConfig['db_pass']);
This gives you absolute peace of mind regarding data isolation. It's physically impossible for Clinic A to see Clinic B's data because they aren't even connected to the same database. It also makes backups and restores trivial; if Client C accidentally deletes their data, you can restore just their specific database without rolling back the entire platform. But be warned: the maintenance overhead is grueling. Imagine having 500 clients and needing to add a single column to a table. You can't just run one migration; you have to loop through 500 databases and run the migration 500 times. If the process crashes halfway through, you now have a "split-brain" schema where half your clients are on version 1.2 and the other half are on 1.3.
Choosing Your Poison Based on the Client
So, which one do you choose? I usually tell people that the decision depends more on your legal requirements and your price point than on the technology itself. If you are building a low-cost tool for freelancers, a shared database is almost always the right choice. The overhead of managing hundreds of databases will kill your productivity, and the risk is relatively low.
However, if you are selling to enterprises, banks, or healthcare providers, they will often demand isolated data. They might even insist that the database be hosted on their own infrastructure. In those cases, the "migration nightmare" is simply the cost of doing business. I've found that if you go the isolated route, investing heavily in an automated migration runner—something that can handle failures gracefully and log exactly which tenants were updated—is not optional; it's a requirement for your sanity.
📋 Practical Task
Exercise: Building a Tenant-Aware Database Connection Switcher
In this exercise, you will implement a TenantManager class that handles the logic of switching database connections based on a provided tenant identifier. This simulates the "Database-per-Tenant" approach.
Requirements:
- Create a
TenantManagerclass that accepts a PDO connection to a "Master" database in its constructor. - Implement a method
getConnectionForTenant(string $tenantIdentifier): PDO. - This method must:
- Query the Master database to find the database name, username, and password associated with that
$tenantIdentifier. - If the tenant doesn't exist, throw a custom
TenantNotFoundException. - Return a new PDO instance connected to that specific tenant's database.
- Query the Master database to find the database name, username, and password associated with that
- Write a small test script that attempts to connect to a "Client_A" and "Client_B" and executes a simple
SELECT 1query to verify the connection is active.
Constraints: Do not hardcode the credentials in the TenantManager; they must be retrieved dynamically from the Master database.
There are no comments for now.