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
176: Building a Multi-Tenant SaaS Backend Structure
I once worked with a junior dev who was building a project management SaaS. He had a "Tasks" table and a "Tenants" table. Everything seemed great during local development because he was only testing with one account. Then, he pushed to staging, created two different client accounts, and realized that when Client A logged in and clicked "View All Tasks," they saw Client B's private project milestones and internal notes. He'd committed the cardinal sin of multi-tenant development: forgetting the tenant_id in a WHERE clause.
// The "Nightmare" Code
public function index() {
// He thought he was getting the current user's tasks...
// But he actually fetched every single task in the entire database
$tasks = Task::all();
return view('tasks.index', ['tasks' => $tasks]);
}
The Danger of Manual Filtering
The bug here is obvious once you see it, but it's incredibly easy to repeat. If you rely on yourself (or your team) to remember to add ->where('tenant_id', $tenantId) to every single query in your application, you will eventually miss one. In a SaaS environment, a missing WHERE clause isn't just a bug; it's a critical security breach that can get your company sued or shut down.
The fix isn't just to add the missing line of code to that one controller. The fix is to move the responsibility of tenant isolation out of the controller and into the data layer itself, so it happens automatically.
Enforcing Isolation with Global Scopes
If you're using an ORM like Eloquent in Laravel (or a similar pattern in other PHP frameworks), the most robust way to handle this is through a Global Scope. I prefer this because it makes the isolation "invisible" to the developer writing the business logic.
// 1. Create a Trait to handle the logic
trait BelongsToTenant {
protected static function bootBelongsToTenant() {
static::addGlobalScope('tenant', function ($builder) {
// We assume there is a TenantManager class that knows who is logged in
$builder->where('tenant_id', TenantManager::getTenantId());
});
// Automatically assign tenant_id when creating new records
static::creating(function ($model) {
$model->tenant_id = TenantManager::getTenantId();
});
}
}
// 2. Use it in your Model
class Task extends Model {
use BelongsToTenant;
}
Now, when you call Task::all(), the underlying SQL becomes SELECT * FROM tasks WHERE tenant_id = 5. You don't have to think about it, and you can't "forget" it. I've found this approach saves hours of debugging and prevents those midnight panic calls when a client reports seeing someone else's data.
Choosing Your Isolation Strategy
While the shared-database approach (row-level isolation) we just discussed is the most common for early-stage SaaS, it's not the only way. Depending on your clients' needs—especially if you're dealing with healthcare or high-finance data—you might need a stricter boundary.
- Shared Database, Shared Schema: What we just did. Fast to scale, easy to migrate, but carries the risk of data leaks if your scopes are buggy.
- Shared Database, Separate Schemas: Each tenant gets their own schema (e.g., in PostgreSQL). It's a middle ground that provides better isolation but makes running migrations across 1,000 tenants a headache.
- Database per Tenant: The gold standard for security. Each client has a completely separate physical database. If one database gets corrupted, others are safe. However, it's expensive to maintain and a nightmare to manage connections at scale.
My advice? Start with row-level isolation and a strict Global Scope. Only move to separate databases if you have a specific contractual requirement for "physical data separation" or if your database size per tenant is so massive that it's slowing down the rest of your users.
📋 Practical Task
Implement an Automated Tenant-Aware Document Repository
You are building a "Secure Document Storage" SaaS. You have a Document model and a TenantManager class that stores the current active tenant_id in the session.
Your Task:
- Create a PHP Trait named
Multitenantthat implements a mechanism to automatically filter all queries by thetenant_idprovided byTenantManager::getId(). - Ensure that whenever a new
Documentis saved to the database, thetenant_idis automatically injected so the developer doesn't have to set it manually in the controller. - Implement a method within the trait or model called
withoutTenantScope()that allows an administrator (and only an administrator) to fetch documents across all tenants for reporting purposes.
Provide the code for the Trait and the Model implementation.
There are no comments for now.