I am currently working on a project for which we have chosen Yii as our new Framework of choice. I am currently trying to figure out the best way to implement some sort of automatic database fail over in Yii.
I am currently trying to over-ride the CDbConnection class - Open function. I am not sure if I am headed in the right direction though.
Basically what I am looking to do is check a DB connection and if it fails connect to another DB. Simple concept I am just not sure where to put it. I know there are better ways to do this by using mysqlnd_ms but it is not setup on the servers we are using yet so have to come up with a way to do this in Yii. Any help is greatly appreciated. -DA
This is what I have so far Thoughts?
class DaDbConnection extends CDbConnection{
public $dbConnectTries = 6;
public $numDatabases = 3;
private $_tries =0;
private $_db = 1;
/*
* Extends CDbConnection open() method
* Tries to connect to database connections setup in config/main.php up to
* the value of $dbConnectionTries or a connection is successful
* #throws CException If it can not connect to any DBs
*/
protected function open()
{
try{
//try to connect to the default DB
parent::open();
}catch(Exception $e){
if($this->_tries < $this->dbConnectTries){
//If there aren't anymore DBs to try we must start over from the first
if($this->_db >= $this->numDatabases){
$tryDb = 'db';
$this->_db = 0;
}else{
$tryDb = 'db'.$this->_db;
}
$this->_db++;
$this->_tries++;
$this->connectionString = Yii::app()->$tryDb->connectionString;
$this->username = Yii::app()->$tryDb->username;
$this->password = Yii::app()->$tryDb->password;
$this->open();
}else{
throw new CDbException('Could Not Connect to a DB.');
}
}
}
}
Sounds like the right direction. I'm not sure Yii has anything built in for that, please someone correct me if I'm wrong.
What I'd probably try, just off the top of my head, is defining the two databases in my main config file but with my own custom class;
return array(
...
'components' => array(
'db' => array(
'connectionString' => 'mysql:host=dbserver1;dbname=my1db',
...
'class' => 'MyCDbConnection',
...
),
'dbBackup' => array(
'connectionString' => 'mysql:host=dbserver2;dbname=my2db',
...
'class' => 'MyCDbConnection',
),
...
),
);
I'd then make the MyCDbConnection class extend the main CDbConnection class but include my own open method, as you suggested.
It is possible to switch between databases quite easily (e.g. Multiple-database support in Yii), and I'm sure you could integrate that into the try/catch of opening the db connection in your custom open() method?
Related
I have a Symfony 3.4 app that could contain multiple companies.
Each company have their own config, and their own data in db, so I need that each company have their own db.
When any user login, The application has a "core database" containing user's info.
After user login the application must change configuration for connect to user company database, that had saved in "core database".
There are necessary steps:
One user enter his user and password
the app look into central db and get user's authentication.
The app get user configuration to change.
The app change the configuration and now, sql request will be to the company's db.
It is possible? If not, is there any alternative?
Thank you so much!
You have to work here with multiple entity managers and connections and and idea is to use a subscriber that retrieves the current customer based on the user. This subscriber (or another service) will set a global variable containing the name of the entity manager.
// A subscriber (high level priority) or a service already set $customerName
// In your controller or in a service
$customerEntityManager = $this->getDoctrine()->getManager($customerName);
Check also this bundle for ideas https://github.com/vmeretail/multi-tenancy-bundle
Edit
Use and adapt to your needs this file https://github.com/vmeretail/multi-tenancy-bundle/blob/master/Service/TenantResolver.php
Here you just need to resolve tenant from the current User.
In your controller:
...
public function index(TenantResolver $tenantResolver)
{
$customerEntityManager = $this->getDoctrine()->getManager($tenantResolver->getTenant()->getName()); // or getId() or something else
}
In a service:
...
use Doctrine\Common\Persistence\ManagerRegistry;
private $tenantResolver;
private $managerRegistry;
public function__construct(TenantResolver $tenantResolver, ManagerRegistry $managerRegistry)
{
$this->tenantResolver = $tenantResolver;
$this->managerRegistry = $managerRegistry;
}
public function doSomething()
{
$this->managerRegistry->getManager($this->tenantResolver->getTenant()->getName()); // or getId() or something else
}
It's the idea, there must be something better to do here like injecting directly the right manager in the service/controller constructor.
I found the following solution for Symfony 4 and i think it should work for symfony 3.4 as well.
I created a service that copies the default entity manager in a new one connecting to another database:
namespace App\Service;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Doctrine\ORM\EntityManagerInterface;
class CustomEntityManagerHelper
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/*
* get entity manager for another database
*/
public function getManagerForDatabase($db_name): EntityManagerInterface
{
$conn = array(
'driver' => 'pdo_mysql',
'user' => 'root',
'password' => 'mypass',
'dbname' => $db_name
);
return \Doctrine\ORM\EntityManager::create(
$conn,
$this->em->getConfiguration(),
$this->em->getEventManager()
);
}
}
Until now it was very easy but the Repository class still uses the default entitymanager. So i added a method setEntityManager to the Repositories:
<?php
namespace App\Repository;
use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
class ProductRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Product::class);
}
public function setEntityManager(EntityManagerInterface $entityManager): self
{
$this->_em = $entityManager;
return $this;
}
// custom methods here
}
Now i can use the custom entity manager AND set that to the repository:
use App\Service\CustomEntityManagerHelper;
// ...
/**
* #Route("/products", name="app_product", methods={"GET"})
*/
public function index(CustomEntityManagerHelper $helper): Response
{
$myManager = $helper->getManagerForDatabase($this->getUser()->getDatabaseName());
$products = $myManager->getRepository('App:Product')
->setEntityManager($myManager) // IMPORTANT!
->findAll();
return $this->render('product/index.html.twig', [
'products' => $products
]);
}
I have a CakePHP application hosted on AWS Elastic Beanstalk. Because of the multiple EC2 instances I will use in the future I want to store my PHP sessions in a database. AWS provides a very nice library for storing PHP sessions in their DynamoDB database. See http://goo.gl/URoi3s
Now I putted the AWS SDK in my vendors folder and created an access wrapper for it (a plugin):
<?php
Configure::load('aws');
require_once VENDORS . 'autoload.php';
use Aws\Common\Aws;
class AwsComponent extends Component
{
private $_aws;
public function __construct()
{
$this->_aws = Aws::factory(array(
'key' => Configure::read('Aws.key'),
'secret' => Configure::read('Aws.secret'),
'region' => Configure::read('Aws.region')
));
}
public function getClient($service)
{
return $this->_aws->get($service);
}
}
The wrapper is working well, I already implemented some S3 stuff. Now for the session handler i added the following code to my AppController.php:
public $components = array('Aws.Aws');
public function beforeFilter()
{
$this->_setSessionStorage();
}
private function _setSessionStorage()
{
$client = $this->Aws->getClient('dynamodb');
$client->registerSessionHandler(array(
'table_name' => 'sessions'
));
}
The AWS's internal registerSessionHandler() is executed (tested it) but the session is not beeing stored into the DynamoDB table. Of course I created the table before and if I add the call to the AWS library directly to my webroot/index.php before dispatcher is loaded everything works fine.
I think the problem is that my code is executed after CakePHP calls session_start(). So what is the best way to implement that? http://goo.gl/kUFUIR doesn't help me, I don't want to rewrite the AWS library for beeing compatible with the CakePHP interface.
So what is the best way to implement that? http://goo.gl/kUFUIR
doesn't help me, I don't want to rewrite the AWS library for beeing
compatible with the CakePHP interface.
This is in fact the best way. And this does not mean to reinvent the wheel, abstraction in OOP means that you make things available in a generic interface that can be replaced with something else. You wrap a foreign API or code in an API compatible to your system, in this case a CakePHP application.
Wrap the vendor lib in a AwsSession adapter that implements the CakeSessionHandlerInterface. This way it's API compatible with other session adapters in the case you change it and it might be even solve your core problem, because CakeSession will take care of the initialization.
Your component is initialized after the session in CakePHP, when the controller is already instantiated and then is initializing all its components. So this happens at a pretty late time. Your alternative is to stop CakePHP from initializing the session, I never had a need to do so, so no idea without looking it up myself. Dig in CakeSession. Even if you manage to do so, other components like the default Auth adapter depends on being able to work with Sessions, so you have to take care of the issue that your component has to be loaded before Auth as well. Pretty fragile system with lots of possbile points of failure. Seriously, go for the Session adapter, guess its a lot less painful to get it working this way.
By a quick look at the DynamoDB Session documentation this seems to be pretty easy. Extend the regular session handler and overload only the init and garbage collection of it to add the Aws API calls there, no guarantee this is right but seems to be easy.
What I end up with in CakePHP 3.
src/Network/Session/DynamoDbSession.php
<?php
namespace App\Network\Session;
use Aws\DynamoDb\DynamoDbClient;
use Cake\Core\Configure;
class DynamoDbSession implements \SessionHandlerInterface
{
private $handler;
/**
* DynamoDbSession constructor.
*/
public function __construct()
{
$client = new DynamoDbClient(Configure::read('DynamoDbCredentials'));
$this->handler = $client->registerSessionHandler(array(
'table_name' => Configure::read('DynamoDbCredentials.session_table')
));
}
public function close()
{
return $this->handler->close();
}
public function destroy($session_id)
{
return $this->handler->destroy($session_id);
}
public function gc($maxlifetime)
{
return $this->handler->gc($maxlifetime);
}
public function open($save_path, $session_id)
{
return $this->handler->open($save_path, $session_id);
}
public function read($session_id)
{
return $this->handler->read($session_id);
}
public function write($session_id, $session_data)
{
return $this->handler->write($session_id, $session_data);
}
}
Activate it in config/app.php file:
'Session' => [
'defaults' => 'php',
'handler' => [
'engine' => 'DynamoDbSession'
],
'timeout' => (30 * 24 * 60)
]
I have to work with an Oracle database using the old database driver (ora_logon ) which is not supported by cakephp. I cant use the oci driver instead.
Right now I do the follow:
Every method of every model connects to the database and retrieve data
class SomeClass extends Model {
public function getA(){
if ($conn=ora_logon("username","password"){
//make the query
// retrieve data
//put data in array and return the array
}
}
public function getB(){
if ($conn=ora_logon("username","password"){
//make the query
// retrieve data
//put data in array and return the array
}
}
}
I know that it is not the best way go.
How could I leave cakephp manage opening and closing of the connection to the database and have models only retrieve data? I'm not interested in any database abstraction layer.
I would think you could just make your own OracleBehavior. Each model could use this behavior, and in it, you can overwrite or extend the Model's find() behavior to build a traditional oracle query and run it (I don't know much about Oracle).
Then, in your Behavior's beforeFind() you can open your connection, and in your Behavior's afterFind(), you can close your database connection.
That way, every time before a query is run, it automatically opens the connection, and every time after a find it closes it. You can do the same with beforeSave() and afterSave() and beforeDelete() and afterDelete(). (You'll likely want to create a single connect() method and disconnect() method in the Behavior, so you don't have duplicate code in each beforeX() method.
Do you really need to extend a Cake Model class?
class SomeClass extends Model {
private $conn;
public function constructor() {
parent::constructor();
$conn = ora_logon("username","password");
if(!$conn)
throw new Exception();
}
public function getA() {
//Some code
}
}
SomeController:
App::uses('SomeClass','Model');
public function action() {
$data = array();
$error = null;
try{
$myDb = new SomeClass();
$data = $myDb->getA();
} catch($e) {
$error = 'Cannot connect to database';
}
$this->set(compact('data', 'error'));
}
I'm using DB logging in Cake 2.1, which works great.
The problem I'm having is when running Unit Tests, all logs are still getting sent to the live db rather than the test database.
All other db interactions go to test, except logging.
I do have a log fixture created and imported into the test case.
Here's my Database logger (/Lib/Log/Engine/DatabaseLogger.php)
App::uses('CakeLogInterface', 'Log');
class DatabaseLogger implements CakeLogInterface
{
public function __construct($options = array() )
{
App::import('Model', 'Log');
$this->Log = new Log;
}
public function write($type, $message)
{
$this->Log->create();
$log['type'] = ucfirst($type);
$log['date'] = date('Y-m-d H:i:s');
$log['message'] = $message;
return $this->Log->save($log);
}
}
I'm sure I'm missing some basic setting here but I can't figure this out for the life of me.
Well, in my case the problem was caused because of a bad initialization of a constructor.
You can check the update solution here:
How to choose the test DB cakePHP testing
And here:
How to override model's constructor correctly in CakePHP
I have just finished my first site built with zend framework and all works great on my local machine.
Then I uploaded it to the server (godaddy) and all works except any connection my models do with the database. I have made a connetion to the database with regular PDO with the credentials in my application.ini and it worked, and I can interact with the model if it's not returning anything from the database (and again all the models work great on my local machine).
My models looks like this:
class Default_Model_picture extends Zend_Db_Table_Abstract
{
protected $_name = 'pictures';
protected $_primary = 'id';
public function getPicturesByCategory($category)
{
$query = $this->select()->from(array('pictures'), array(
'pictures.id', 'pictures.pic_name', 'pictures.pic_desc',
'pictures.pic_category', 'pictures.pic_date_added',
'pictures.pic_larger', 'pictures.pic_url'));
$query->where('pic_category = ?', $category);
$query->order('pic_date_added ASC');
$result = $this->fetchAll($query);
return $result;
}
}
this is an example for a model, obviously i did not added lots of methods.
i have no idea what to do next.
I am assuming you set up the db connection correctly into $db. Afterwards you must set it as the default adapter for Zend_Db_Table.
Zend_Db_Table::setDefaultAdapter($db);
I am just assuming this is what went wrong. But it is a common problem, so I decided to go ahead and answer anyway.
Since your script works fine on your local machine, the first thing I check is if you have got the database connection params setup correctly in your application.ini
Try to write a test script that uses the pdo functions on itself (without zend framework). see if you get any errors at all
try {
$dbh = new PDO('mysql:host=YOURHOST;dbname=YOURDBNAME', $YOURUSERNAME, $YOURPASSWORD);
foreach($dbh->query('SELECT * from FOO') as $row) {
print_r($row);
}
$dbh = null;
} catch (PDOException $e) {
print "Error!: " . $e->getMessage() . "<br/>";
die();
}