Laravel 5.4 Multi-Tenant migrations, switching connections for migrations - php

I have a multi-tenant app that has a master database with client info, this has settings for different databases. By reading the URL on startup I can figure out the tenant they are accessing, then switch the connection and cache the settings so that it is using the correct tenants database. This part works but the problem comes up with migrations.
The regular Laravel migrations only handle the master table, so I added a migration folder for "tenants" this needs to run on all tenants for each update. To do this I use the following class
<?php
namespace App\Console\Commands\Tenants;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class UpdateTenant extends Command
{
protected $signature = 'tenant:update {slug}';
protected $description = 'Update a tenants database';
protected $migrator;
public function __construct()
{
parent::__construct();
$this->migrator = app()->make('migrator');
}
public function fire()
{
$arguments = $this->arguments();
if ($account = DB::connection('root')->table('accounts')->where('slug', '=', $arguments['slug'])->first()) {
config()->set('database.connections.tenant.database', $arguments['slug']);
$this->migrator->setConnection('tenant');
if (! $this->migrator->repositoryExists()) {
$this->call('migrate:install', ['--database' => 'tenant']);
}
$this->migrator->run([$this->laravel->basePath() . '/' . 'database/tenants']);
foreach ($this->migrator->getNotes() as $note) {
$this->output->writeln($note);
}
}
}
}
As you can see this is just for one tenant defined by the slug, I have another command to loop and call the artisan command on all tenants.
foreach ($accounts as $account) {
$this->call('tenant:update', ['slug' => $account->slug]);
}
The issue here is that although checking the value of the slug it is correctly finding the correct tenant info, the connection gets stuck on the first tenant despite switching the connection. Even if I try to switch it to the root and back it just ignores the change to the config. Is there anyway to tell laravel to reset the connection so that it will use the updated values in the config to reconnect?

Disconnect from the current connection in between tenants with:
config()->set('database.connections.tenant.database', $arguments['slug']);
DB::disconnect('tenant');<----- add this
$this->migrator->setConnection('tenant');
This will clean up the resource connection and force the app to re-establish itself with the correct configuration settings.

Related

Connection Refuse exception occur in drive: SQLSTATE[HY000}[2002]

So I am starting a docker symfony 4 project and i am trying to use a controller. I already created the database that i need by running
doctrine:database:create
and it work and created my database so the connection is good. however, when i run this code below. I get that error. I a little confuse on why mysql doesnt connect. anyone know?
Ive tried change mysql versions, composing it down and updating different mysql root and password and is not connecting.
class HospitalAdminController extends AbstractController
{
/**
* #Route("/admin/hospital/new")
*/
public function new(EntityManagerInterface $em)
{
$hospital = new Hospital();
$hospital->setName('Example Hospital')
->setPhone(8175831483)
->setAddress('123 Avenue');
$em->persist($hospital);
$em->flush();
return new Response(sprintf(
'Hiya! New Hospital id: #%d phone:%s address%s',
$hospital->getId(),
$hospital->getPhone(),
$hospital->getAddress()
));
}
}
just trying to run my controller and do a proof of concept.

Yii2/PHP: Check connection to MySQL and PostgreSQL

When starting my Yii2/PHP application, how can I check if / wait until the database is up?
Currently with MySQL I use:
$time = time();
$ok = false;
do {
try {
$pdo = new PDO($dsn,$username,$password);
if ($pdo->query("SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA"))
$ok=true;
} catch (\Exception $e) {
sleep(1);
}
} while (!$ok && time()<$time+30);
Now I want make my application running with MySQL and PostgreSQL.
But SELECT 1 FROM INFORMATION_SCHEMA.SCHEMATA does not work in PostgreSQL.
Is there a SQL-statement (using PDO database connectivity) that works on both database systems to check if the database is up and running?
Yii2 has a property to verify if a connection exists or not, it is really not necessary to create a script for this, since this framework has an abstraction implemented for the databases it supports ($isActive property).
$isActive public read-only property Whether the DB connection is
established
public boolean getIsActive ( )
You can do the check in your default controller in the following way:
<?php
class DefaultController extends Controller
{
public function init()
{
if (!Yii::$app->db->isActive) {
// The connection does not exist.
}
parent::init();
}
}
It is not good practice to force waiting for a connection to a database unless there are very specific requirements. The availability of a connection to the database must be a mandatory requirement for an application to start and the application should not "wait" for the database to be available.
There are ways to run containers in docker in an orderly manner or with a specific requirement, this link could give you a better option instead of delegating this to the application.
You could use SELECT 1 which is standard SQL.
You can use dbfiddle to test against various servers.
The server could go away an any time so checking the error response with every query is a much better approach.

Laravel 5.4 MultiSite with Queues per site

My Laravel is setup with a MultiSite Middleware Provider that checks the subdomain of the address and based on this subdomain changes the connection on-the-fly to another database.
e.g.
Config::set('database.connections.mysql.host', $config['host'] );
Config::set('database.connections.mysql.database', $config['db_name'] );
Config::set('database.connections.mysql.username', $config['user']);
Config::set('database.connections.mysql.password', $config['password']);
Config::set('database.connections.mysql.prefix', $config['prefix']);
Config::set('database.connections.mysql.theme', $config['theme']);
// purge main to prevent issues (and potentially speed up connections??)
DB::disconnect('main');
DB::purge();
DB::reconnect();
return $next($request);
This all works fantastic, except that I now want to use Laravel Queues with the built-in Database driver (sync actually works fine but blocks the user experience for long report generations).
Except Artisan isn't sure which database to connect to so I'm guessing it connects to the default, which is a kind of supervisor database that stores all the subdomains and corresponding db names etc.
Note none of these databases are setup in my database conf as connections, they're stored in a singular management database as there's quite a lot of them.
I've tried cloning the built-in Queue listener and modifying it to swap to the different site connection as so:
/**
* Create a new queue listen command.
*
* #param \Illuminate\Queue\Listener $listener
* #return void
*/
public function __construct(Listener $listener)
{
// multisite swap
$site = MultiSites::where('machine_name', $this->argument('site'));
MultiSites::changeSite($site->id);
parent::__construct();
$this->setOutputHandler($this->listener = $listener);
}
But this fails with
$commandPath argument missing for the Listener class.
Trying a similar database/site swap in the fire() or handle() methods stops the $commandPath error however it simply does nothing, no feedback and doesn't begin to process any jobs from the database.
I'm at a loss how to get this working with a multisite environment, does anyone have any ideas or am I going the wrong way about this?
My ideal scenario would be being able to run a singular Queue command, have supervisor monitor that and it to skip through each database checking. But I am also willing to spawn a queue command per database/site if necessary.

Codeigniter 3 - How to use Forge Class to create a database schema?

I'm using CodeIgniter 3.1.0 to develop an app. In order to improve installation, I've written an Install_Controller, and an Install_Model.
I want to use Database Forge class to manage the database schema, but it's not very clear in user guide and nothing on Google helps.
I need to use $this->dbforge->create_database, because the user knows nothing about database, so all he will do is MySQL "Next, next, install" and then run a batch file that run PHP as web server, so from Chrome he can use URL to install the app.
User guide says: In order to initialize the Forge class, your database driver must already be running, since the forge class relies on it.
So I have setup the config/database.php with user, pwd, dbname and so on... Even because I need it to use in app.
When I try to load the URL to install the app, give me the error: Message: mysqli::real_connect(): (HY000/1049): Unknown database 'test'
So, how can I use Forge Class to create database schema, if I need to have it first?
Some code...
$db['default'] = array(
'dsn' => '',
'hostname' => 'localhost',
'username' => 'root',
'password' => 'root',
'database' => 'test',
'dbdriver' => 'mysqli'
);
$autoload['libraries'] = array('database');
class Install extends CI_Controller
{
public function __construct()
{
parent::__construct();
}
public function index()
{
$this->load->model('install_model');
$this->install_model->createDabase();
}
}
class Install_model extends CI_Model {
function __construct() {
parent::__construct();
}
function createDabase() {
$this->load->dbforge();
if ($this->dbforge->create_database('test'))
echo 'Database created!';
else
echo 'Database error!';
}
}
After try and get a lot of opinions, the only way is as I commented:
Remove database from autoloader;
Use multiple databases, the default one is the same, but create a second one having empty database name;
Use the second database with Forge Class, instead of the default, only for create database and tables;
Change manually for the the default database and use it after that.

Laravel: Use Memcache instead of Filesystem

Whenever I load a page, I can see Laravel reading a great amount of data from the /storage folder.
Generally speaking, dynamic reading and writing to our filesystem is a bottleneck. We are using Google App Engine and our storage is in Google Cloud Storage, which means that one write or read is equal to a "remote" API request. Google Cloud Storage is fast, but I feel it's slow, when Laravel makes up to 10-20 Cloud Storage calls per request.
Is it possible to store the data in the Memcache instead of in the /storage directory? I believe this will give our systems a lot better performance.
NB. Both Session and Cache uses Memcache, but compiled views and meta is stored on the filesystem.
In order to store compiled views in Memcache you'd need to replace the storage that Blade compiler uses.
First of all, you'll need a new storage class that extends Illuminate\Filesystem\Filesystem. The methods that BladeCompiler uses are listed below - you'll need to make them use Memcache.
exists
lastModified
get
put
A draft of this class is below, you might want to make it more sophisticated:
class MemcacheStorage extends Illuminate\Filesystem\Filesystem {
protected $memcached;
public function __construct() {
$this->memcached = new Memcached();
$this->memcached->addServer(Config::get('view.memcached_host'), Config::get('view.memcached_port');
}
public function exists($key) {
return !empty($this->get($key));
}
public function get($key) {
$value = $this->memcached->get($key);
return $value ? $value['content'] : null;
}
public function put($key, $value) {
return $this->memcached->set($key, ['content' => $value, 'modified' => time()]);
}
public function lastModified($key) {
$value = $this->memcached->get($key);
return $value ? $value['modified'] : null;
}
}
Second thing is adding memcache config in your config/view.php:
'memcached_host' => 'localhost',
'memcached_port' => 11211
Last thing you'll need to do is to overwrite blade.compiler service in one of your service providers, so that it uses your brand new memcached storage:
$app->singleton('blade.compiler', function ($app) {
$cache = $app['config']['view.compiled'];
$storage = $app->make(MemcacheStorage::class);
return new BladeCompiler($storage, $cache);
});
That should do the trick.
Please let me know if you see some typos or error, haven't had a chance to run it.

Categories