Eloquent Global Scopes make it easy to customize your Model’s queries, however sometimes it is just not enough.
You may want to use similar solution for you DB::table(..) calls as well. While there’s no built-in feature for this, you can still do it with just a little more effort, and here’s how it’s done.
Imagine multi-tenant app where you want to add tenant context to each query (and let it be row-based context, ie. each table has tenant_id column that defines that context):
|
1 2 3 4 5 6 7 8 9 |
// Eloquent call with global scope applied Client::query()->toSql(); // "select * from clients where tenant_id = ?" // but DB::table('clients')->toSql(); // "select * from clients" |
Unfortunately there is no way to inject custom Builder class to the Connection object, because it’s simply instantiated here:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * Begin a fluent query against a database table. * * @param string $table * @return \Illuminate\Database\Query\Builder */ public function table($table) { $processor = $this->getPostProcessor(); $query = new Query\Builder($this, $this->getQueryGrammar(), $processor); return $query->from($table); } |
It is also not extendable like Eloquent\Builder (MacroableTrait).
That said, we need to use custom Connection class that will call our Builder in overriden table method:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// We need separate classes for each driver, so use a trait: <?php namespace My\Space; use Illuminate\Database\MySqlConnection as BaseConnection; class MySqlConnection extends BaseConnection { use TenantConnectionTrait; } <?php namespace My\Space; trait TenantConnectionTrait { /** * Begin a fluent query against a database table. * * @param string $table * @return \Illuminate\Database\Query\Builder */ public function table($table) { $query = new Builder($this, $this->getQueryGrammar(), $this->getPostProcessor()); return $query->from($table); } } |
Next we can adjust the Builder as we need:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<?php namespace My\Space; use Illuminate\Database\Query\Builder as BaseBuilder; class Builder extends BaseBuilder { // // quite a few methods skipped for brevity // /* |---------------------------------------------------------------- | Query Builder overrides |---------------------------------------------------------------- */ /** * Set the table which the query is targeting. * * @param string $table * @return $this */ public function from($table) { if ($this->from) { $this->updateTenantConstraint($table); } else { $this->applyTenantScope($table); } parent::from($table); return $this; } /** * Get a new instance of the query builder. * * @return static */ public function newQuery() { return new static($this->connection, $this->grammar, $this->processor); } } |
This is the minimum, that we need to override for READ operations. Of course there is much more, like handling joins, disabling scope and so on and so forth. Full code will be available on github soon, so stay tuned!
Anyway, there is still one thing missing here – the most important one: IoC binding. In order to use our custom solution, we need to bind the connection in the Container. This will be done in the service provider:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
<?php namespace My\Space; use Illuminate\Support\ServiceProvider; class TenantServiceProvider extends ServiceProvider { private $drivers = [ 'mysql' => 'MySql', 'pgsql' => 'Postgres', 'sqlite' => 'SQLite', 'sqlsrv' => 'SqlServer', ]; /** * Register the service provider. * * @return void */ public function register() { foreach ($this->drivers as $key => $driver) { $this->bind($key, $driver); } } /** * Bind custom connection in the IoC. * * @param string $key * @param string $driver * @return void */ protected function bind($key, $driver) { $this->app->singleton("db.connection.{$key}", function($app, $deps) use ($driver) { list($connection, $database, $prefix, $config) = $deps; $concrete = "{$driver}Connection"; return new $concrete($connection, $database, $prefix, $config); }); } } |
Complete solution will let you do for example this (no need for Eloquent Global Scopes on your model anymore):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Scope applied by default [1] > DB::table('users')->toSql(); // 'select * from `users` where `users`.`tenant_id` in (?, ?, ?)' // Scope disabled on demand [2] > DB::noTenantScope(function () { return DB::table('users')->toSql();}); // 'select * from `users`' // Joins handled properly [3] > User::join('accounts as a', 'a.id', '=', 'users.account_id')->toSql(); // 'select * from `users` inner join `accounts` as `a` on `a`.`id` = `users`.`account_id` where `users`.`tenant_id` in (?, ?, ?) and `accounts`.`tenant_id` in (?, ?, ?)' // Full support for table aliasing [4] > DB::table('users')->join('accounts as a', 'a.id', '=', 'u.account_id')->from('users as u')->toSql(); // 'select * from `users` as `u` inner join `accounts` as `a` on `a`.`id` = `u`.`account_id` where `u`.`tenant_id` in (?, ?, ?) and `a`.`tenant_id` in (?, ?, ?)' // and much MUCH more |





