Dynamically change database connection in cakephp 3 - php

I'm trying to change the database connection used in cakephp 3 on the fly. Every answer to this question that I found refers to cakephp 2 (These three for instance).
This guy found a solution for cakephp 3 having a finite number of databases, and specifically defining which Database would be used by which Table file.
The thing is that I want to create a new database for every new user, and change to his database when he logs in. I can't know in advance all the databases that will exist, to write it in the config/app.php file.
And I can't set the default database in each /src/Model/Table file, because the tables are the same in every database.

Use the ConnectionManager::config() function to create connections on the fly and the ConnnectionManager::alias() method to make all your Table classes use it by default.
There is a very good article describing the process here:
http://mark-story.com/posts/view/using-cakephp-and-a-horizontally-sharded-database
The only difference is that you can create the connection config on the fly instead of declaring the shards manually as it was shown in that article.

Change database connection for one model:
In app.php :
'test' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Mysql',
'persistent' => false,
'host' => MySQL_HOST,
//'port' => 'nonstandard_port_number',
'port' => MySQL_PORT,
'username' => MySQL_USER,
'password' => MySQL_PASS,
'database' => 'test',
'encoding' => 'utf8',
'timezone' => 'UTC',
'cacheMetadata' => true,
'quoteIdentifiers' => false,
'log' => false,
//'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
]
In Controller :
$conn = ConnectionManager::get('test');
$_model = TableRegistry::get('your_alias', ['table' => 'your_table', 'connection' => $conn]);

Configuring table connections
namespace App\Model\Table;
use Cake\ORM\Table;
class ArticlesTable extends Table
{
public static function defaultConnectionName() {
return 'replica_db';
}
}

Related

Possible to Dynamically Connect to Postgres DB from Laravel without using Config ? See Example

I have a situation where I'd like to dynamically choose the DB host for postgres DB connections in a Laravel application running in Docker. I don't particularly want to use ENV values or the config/database.php file because it is a one-off query to the external DB that I would prefer to embed in Helper Class.
These actually could go in the .env file:
PSQL_DB_PORT=5432
PSQL_DB_DATABASE=postgres
PSQL_DB_USERNAME=postgres
PSQL_DB_PASSWORD=postgres
and in my helper I can get the hostname for the remote (actually in Docker) postgres server, which will basically be something like:
postgres_index-db1, postgres_index-db2, postgres_index-db3, etc., but I need to fetch that dynamically and store it in a variable $host.
I tried playing around with a thing like this:
DB::disconnect('pgsql');
Config::set("database.connections.pgsql", [
'driver' => 'pgsql',
'host' => $host ,
'port' => env('PSQL_DB_PORT', '5432'),
'database' => env('PSQL_DB_DATABASE'),
'username' => env('PSQL_DB_USERNAME'),
'password' => env('PSQL_DB_PASSWORD'),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'schema' => 'public',
'sslmode' => 'prefer',
]);
dd(DB::connection('pgsql'));
$query = "select DISTINCT value from maindicomtags where taggroup = 8 and tagelement = 128";
$names = DB::connection('pgsql')->select($query,[]);
and the dd(DB::connection('psql')) (a Laravel dump) statement actually shows the correct config, but when the query tries to execute it must be looking for the pgsql config in my config/database.php file because it throws an error:
Database connection [pgsql] not configured
because it must be looking for the config in the config/database.php file, and I removed the definition from there. I want it to use the connection that is defined above.
Is there a a way to do what I am trying to do. I could even just use the connection string for Postgres if that is possible.
Might actually be working because I might have had a typo earlier (e.g. psql vsl pgsql).
Does this seem like an adequate way to make that type of dynamic connection ? It is really just the host that will be different.
Instead of setting your config, just create a custom database connection using factory. The only downside - you will not be able to use Eloquent as it resolves connection by its name defined in configs. Also, avoid using env variables anywhere else than config files - create other config and use it instead. There is quite a chance something could break somewhere else if you change config on the go:
$factory = app(\Illuminate\Database\Connectors\ConnectionFactory::class);
$db = $factory->make([
'driver' => 'pgsql',
'host' => $host ,
'port' => $port ?? 5432,
'database' => $database,
'username' => $username,
'password' => $password,
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'schema' => 'public',
'sslmode' => 'prefer',
]);
$names = $db->table('maindicomtags')
->where('taggroup', 8)
->where('tagelement', 128)
->distinct()
->pluck('value');
$db->disconnect();

Yii2 : How to change db connection string dynamically?

I want to change databases connection string and its credentials
on runtime.
I Have master database and many children databases.
And want to switch databses string and its credentials stored in master DB.
not like this Yii::$app->db, Yii::$app->db2, .. connections name
I found the solution to switch DB
Yii provides to set or override component by Yii::$app->set() function.
So we can override it any where within application.
In case of globally change the database, we can override it in on beforeRequest accrding to condition
In config/main.php
'components'=>[
...
],
'on beforeRequest' => function() {
$model = app\models\MainFY::findOne(Yii::$app->session->get('activeSession'));
Yii::$app->set('db', [
'class' => 'yii\db\Connection',
'dsn' => $model->db_string,
'username' => $model->db_username,
'password' => $model->db_password,
'charset' => 'utf8',
'enableSchemaCache' => !YII_DEBUG,
]);
},
in model app\models\MainFY , I am using main DB connection
public static function getDb() {
return Yii::$app->dbMain;
}

How to create a new database by Yii2 migration, when I have to use it by default?

I have a task which is described like "Creating a new database with Yii2 migration, and use it by default". But I have some collusion: I have to set up at lest one default database in my main-local.php file to start migration. But also I have to create this database with initial migration.
return [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=default_base',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
];
How can I deal with it? To create a database with migration and set up as default one.
When I execute my migration (there is code from my migration file):
public function safeUp()
{
$this->execute("CREATE DATABASE default_base");
}
I have an exception:
General error: 1007 Can't create database 'default_base'; database exists
I realize that is because I have already created default_base in phpmyadmin (click button "created database"), and connected it with app in my main-local.php file.
But I need this base to be created only when I execute yii migrate.
UPD
I have tried the way rob006 recommended but I get connection error
Exception 'yii\db\Exception' with message 'SQLSTATE[HY000] [1049] Unknown database 'default_base''
My main-local.php looks like the way exactly rob006 recommends, but I still can`t migrate. It works only if I set:
'preinstallDb' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=already_exists_db',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
With the already exists database. What do I do wrong again?
You can define DSN without database name:
'dsn' => 'mysql:host=localhost',
And use this connection to create database.
Yii::$app->preinstallDb->createCommand('CREATE DATABASE default_base')->execute();
However you can't really create database in migration - MigrateController needs database for migration table to save migrations history. You would need to create database before MigrateController will try to access migration table. You can do this by overriding MigrateController::getMigrationHistory():
class MigrateController extends \yii\console\controllers\MigrateController {
protected function getMigrationHistory($limit) {
Yii::$app->preinstallDb->createCommand('CREATE DATABASE IF NOT EXISTS default_base')->execute();
return parent::getMigrationHistory($limit);
}
}
And use your controller in console app config:
'controllerMap' => [
'migrate' => [
'class' => MigrateController::class,
],
],
But honestly, it is ugly solution with a lot of hardcoding, it may be better to create some setup utility which will perform database creation outside of main app. Also usually for security reasons DB user used by application should not even have privileges to create database, so you may reconsider your use case - do you really need to create database by app?
I find the only way make it work for me. Just overriding init() in MigrateController.
First I added controllerMap in main-local.php file:
'controllerMap' => [
'migrate' => [
'class' => 'console\controllers\MigrateController',]]
Then put new controller in console\controllers:
namespace console\controllers;
use yii\console\controllers\MigrateController as BasicMigrateController;
use Yii;
class MigrateController extends BasicMigrateController
{
public function init()
{
Yii::$app->preinstallDb->createCommand('CREATE DATABASE IF NOT EXISTS default_base')->execute();
parent::init();
}
}

cakephp - compare table with a similar table with uppercase name from another database

I have a table "assets" having (id, name, serial) in my database, and a read-only table ASSET (in uppercase) (ID, NAME, SERIAL) stored in an external database (declared in app.php)
in my app.php, I put the connections:
'Datasources' => [
'default' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Mysql',
'persistent' => false,
'host' => 'localhost',
'username' => 'root',
'password' => '',
'database' => 'db1',
'encoding' => 'utf8',
'quoteIdentifiers' => false,
'url' => env('DATABASE_URL', null),
],
'external_db' => [
'className' => 'Cake\Database\Connection',
'driver' => 'Cake\Database\Driver\Mysql',
'persistent' => false,
'host' => 'domain.com',
'port' => '3306',
'username' => 'my_user',
'password' => 'my_password',
'database' => 'db2',
'encoding' => 'utf8',
'timezone' => 'UTC',
'flags' => [],
'cacheMetadata' => false,
'quoteIdentifiers' => false,
'log' => false,
'url' => env('DATABASE_URL', null),
],
I was able to: cake bake all assets (from the default database).
Since uppercase is not supported with cake bake all, I created manually:
ExternalAssetsTable.php :
class ExternalAssetsTable extends Table {
public function initialize(array $config)
{
parent::initialize($config);
$this->ExternalAsset->setDataSource('external_db');
$this->setTable('ASSET');
$this->setDisplayField('NAME');
$this->setPrimaryKey('ID');
Then, I tried to create the controller and the template views with:
cake bake controller external_assets
cake bake template external_assets
and with:
cake bake controller ASSET -c external_db
cake bake template ASSET -c external_db
and the code did not work.
My first question: What was wrong in my code?
My second need is the following:
"assets" contains data imported from ASSET, and other data entered manually in my database.
SERIAL may change occasionally, and I need to import it into assets. so foreach name = NAME, if serial <> SERIAL then serial := SERIAL.
In assets index.ctp and view.ctp, I need to show the following columns: id, name, serial, and the corresponding ASSET.NAME, ASSET.SERIAL if they exist.
I tried to add a function in AssetsController.php
public function getRemoteData(){
$conn1 = ConnectionManager::get('k1000db'); #Remote Database 1
$sql = "SELECT * FROM ASSET";
$query = $conn1->prepare($sql);
$query->execute();
$result = $query->fetchAll(); #Here is the result
}
My question is: how to call this function in AssetsController index(), view() functions, and in Assets index.ctp ?
Your help is much appreciated.
In your first question:
The cake bake make everything based on the tables that you have on your database, so, your code is:
cake bake ASSETS -c your_external_db_here
If use this code above, you check all the tables that you can bake by Cakephp:
cake bake -c your_external_db_here
And after you make it, manually rename your ASSETS to ExternalAssets, on Controller, Template folder and other things you baked to identify better the tables. But be careful to this cake bake doesn't replace your other assets that you baked before.
In the second question:
You will make a Association, in your AssetTable:
public function initialize(array $config)
{
parent::initialize($config);
$this->hasOne('ExternalAssets')->setForeignKey('fk_here')->('pk_here');
}
In your Controller:
public function index()
{
$assets = $this->Assets->find('all')
->select([ 'id', 'name', 'serial'])
->contain(['ExternalAssets' => [
'fields' => [
'NAME','SERIAL'
]
]);
//if you wanna paginate your resut
//$assets = $this->paginate($assets);
$this->set(compact('assets'));
$this->set('_serialize', ['assets']);
}
public function view($id = null)
{
$assets= $this->Assets->get($id, [
'contain' => []
]);
$this->set('assets', $assets);
$this->set('_serialize', ['assets']);
}
It was not possible for me to cake bake the external table because it did not respect the cakephp conventions. So as an alternative, I used this, in my index():
$connection = ConnectionManager::get('db2');
$machines = $connection->execute('SELECT * FROM MACHINE ORDER BY NAME ASC');
$this->set(compact('machines'));
$this->set('_serialize', ['machines']);
then, I created the views manually.

Laravel 4 - Connect to other database

I want to connect to another database sometimes.
I created a config.php with the database connection data.
But how can i tell laravel to connect to this database insted of using the config/database.php?
For example when using the Schema class.
Since no one seems to understand what i want.
I DON'T want to use the config/database.php, i want to use a different config file on a different location.
It sounds like you figured this out. Here's how I'd accomplish it anyway for other people coming in, or in case something useful is here for you.
First, Add a second connection in app/config/database.php. Note: That file path may change depending on your environment.
<?php
return array(
'connections' => array(
'mysql' => array(
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'database1',
'username' => 'user1',
'password' => 'pass1'
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
),
'mysql2' => array(
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'database2',
'username' => 'user2',
'password' => 'pass2'
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
),
),
);
Second, in your code, you can use (as mentioned) the 2nd connection where you would like:
Schema::connection('mysql2')->create('users', function($table) {})
There's more documentation on this - see Accessing Connections.
Eloquent ORM
You can define the variable for "connection" in an eloquent class to set which connection is used. That's noted in the Basic Usage section.
See that variable on here on Github and the method which you can set to set the connection dynamically here.
Edit
The OP has made it clear that they do not wish to use the config/database.php file for config.
However without explaining further, I can't comment. I'm happy to help - sounds like it would be useful to know why the config/database.php file can't/shouldn't be used, as this can help us ascertain the problem and create a useful solution.
I believe you want to implement some kind of logical sharding where databases would be dynamically created.
In such scenario in laravel you can dynamically add a database config, like below
$conn = array(
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'DATABASE',
'username' => 'USERNAME',
'password' => 'SOME_PASSWORD',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
);
Config::set('database.connections.DB_CONFIG_NAME', $conn);
Now to connect via eloquent
MODEL::on('DB_CONFIG_NAME')->WHAT_EVER('1');
Incase of Query Builder you can do
$DB = DB::connection('DB_CONFIG_NAME');
use $DB->select() for querying now.
Hope this would help devs looking for a possible solution for this question
Remember that Laravel 4 is actually a collection of components, and you can use these components solo.
https://github.com/illuminate/database
There is an example here that shows off how interacting with the Capsule class works:
use Illuminate\Database\Capsule\Manager as Capsule;
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'database',
'username' => 'root',
'password' => 'password',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
]);
// Set the event dispatcher used by Eloquent models... (optional)
use Illuminate\Events\Dispatcher;
use Illuminate\Container\Container;
$capsule->setEventDispatcher(new Dispatcher(new Container));
// Set the cache manager instance used by connections... (optional)
$capsule->setCacheManager(...);
// Make this Capsule instance available globally via static methods... (optional)
$capsule->setAsGlobal();
// Setup the Eloquent ORM... (optional; unless you've used setEventDispatcher())
$capsule->bootEloquent();
This is a bunch of bootstrapping that you'll need to run, so tuck it away somewhere in a function or method.
But, its absolutely possible.
There is a simpler solution. If you are using Larave 4 there is an option that worked for me. Recently they added $table variable that you can specify in your model. Refer to this link.
class User extends Eloquent {
protected $table = 'my_users';
}
If you are using MySQL, you can do the following:
class User extends Eloquent {
protected $table = 'mydbname.my_users';
}
If you are using SQL Server, you can do this:
class User extends Eloquent {
protected $table = 'mydatabase..my_users';
}
My Config file had DB1 specified but I created a model which wants to access DB2 in the same MySQL host. So this was a quick and dirty way to accomplish this.
Now I don't fully leverage Eloquent ORM all the time so this "hack" may not work with Many to Many or one to Many Eloquent methods.
One another idea I had but I didn't actually try was creating a stored procedure (routine) in DB1 and inside of that routine I can access DB2 tables by querying link this:
SELECT * from db2.mytable where id = 1;
To use a config file in another location, say src/config:
use Config;
$this->dbConfig = Config::get('appname::dbInfo.connections.test');
$this->database = $this->dbConfig['database'];
$this->username= $this->dbConfig['username'];
$this->password= $this->dbConfig['password'];
Where dbInfo is a simple php file in your app's src/config directory returning an array containing the element connections which is an array of database properties.
You can tell Laravel to use an external config file using:
Config::set("database.connections.test", $this->dbConfig);
DB::connection("test");
Edit bootstrap/start.php file and add your machine name (open terminal: hostname).
Add your machine to $env,
$env = $app->detectEnvironment(array(
'mymachine' => array('mymachine.local'),
));
Create a new path at 'app/config/mymachine'
Add a copy of database.php with new config params.

Categories