Can't figure this one out:
In the DB_driver.php (core system/database)
this is beginning of init function...
/**
* Initialize Database Settings
*
* #return bool
*/
public function initialize()
{
/* If an established connection is available, then there's
* no need to connect and select the database.
*
* Depending on the database driver, conn_id can be either
* boolean TRUE, a resource or an object.
*/
if ($this->conn_id)
{
return TRUE;
}
// ----------------------------------------------------------------
// Connect to the database and set the connection ID
$this->conn_id = $this->db_connect($this->pconnect);
The initialize() is called wihtin CI_Controller so when creating new objects for example like this:
$x = new PartyPooper();
$y = new PartyPooper();
initialize() in the DB_driver is called twice. Nothing strange with that, but I would expect $this->conn_id to be set when creating PartyPooper() object second time ($y)?
When is "an established connection" supposed to be true? (In the example two database connections are made when there would should only be one?)
I'm using the latest database drivers in the development branch: https://github.com/EllisLab/CodeIgniter/tree/develop/system/database
I am using the mysqli-driver with persistant connections of.
UPDATE:
I'm not a huge fan messing with core-files, but I couldn't figure out another solution here. Please tell me if there's a better way of achieving of what I want to do.
I came up with this code (using sessions to handle storing and checking for existing db-connections (of the "subdriver"-object. (mysqli object in my case)):
public function initialize()
{
/* If an established connection is available, then there's
* no need to connect and select the database.
*
* Depending on the database driver, conn_id can be either
* boolean TRUE, a resource or an object.
*/
if ($this->conn_id) {
return TRUE;
}
$conn_session_id_name = 'dbsession_conn';
if (isset($_SESSION[$conn_session_id_name])) {
$sess = $_SESSION[$conn_session_id_name];
// Set connection id object or resourse and return true
// because no more connecting has to be done
if (is_object($sess) || is_resource($sess)) {
$this->conn_id = $sess;
return TRUE;
}
}
// Connect to the database and set the connection ID
$this->conn_id = $this->db_connect($this->pconnect);
// Store conn object or resource etc into session
if (is_object($this->conn_id) || is_resource($this->conn_id)) {
$_SESSION[$conn_session_id_name] = $this->conn_id;
}
// No connection resource? Check if there is a failover else throw an error
if ( ! $this->conn_id)
{
//rest of code as before...
My application got extremly much faster because it used one connection instead of around 60.
But my original question remains:
When is this supposed to be true in the initialize() function?
if ($this->conn_id) {
return TRUE;
}
(I didn't remove it when changing the code because I suppose it has some purpose-but even if I can't figure out which)
UPDAtE2 - clarification:
In a model I have a db select statement that is supposed to return PartyPooper objects:
eg. $res = $q->result('PartyPooper');
This PartyPooper is a controller that stores information about people, like names, years , birthdays etc, but it also handles stuff like calculation of info within the same object.
class PartyPooper extends CI_Controller { .... }
But as I understand from the comments below I should do like this instead?
class PartyPooper extends CI_Controller { .... }
class PartyPooperObject { .... } //Store information about people in this object
eg. $res = $q->result('PartyPooperObject');
This piece of code
if ($this->conn_id) {
return TRUE;
}
just assures that the initialize method is only called once per object.
If it has been called on the current object already, it must not be executed another time.
Related
Good afternoon,
I have a bunch of legacy code that uses the old mysql library (mysql_query($sql) for example) and am trying to test it with PHPUnit (4.8 is the latest that will run on the server for various reasons).
Does anyone know how to mock this database connection so that it can return predetermined results when predetermined queries are run? I have tried using getConnection() (as per the docs here: http://devdocs.io/phpunit~4/database) to no avail.
For example I have this class:
class Raffle {
...
public static function loadAll($filter=""){
//$filter = mysql_real_escape_string($filter); // protect against SQL injection
$raffles = []; // the array to return
$sql = "SELECT * FROM raffles $filter;"; // include the user filter in the query
$query = mysql_query($sql);
//echo mysql_error();
while($row = mysql_fetch_assoc($query)){
$raffles[] = Raffle::loadFromRow($row); // generate the raffe instance and add it to the array
}
return $raffles; // return the array
}
...
}
(The mysql_connect() call is done in a file called db.php which is loaded on every page that it is needed and not in the class file itself.)
Thanks in advance.
For anyone else stuck in this situation I found that building a mock for PDO that contains a fallback to the functional api allowed me to migrate the code to a Testable OO based model while still using the old mysql library under the hood.
For example:
// the basic interfaces (function names taken from PDO
interface DBConnAdapter {
public function query($sql);
}
// since PDO splits connections and statements the adapter should do the same
interface DBQueryAdapter {
public function num_rows();
public function fetch_assoc();
}
...
class DBAdapter implements DBConnAdapter {
public function query($sql){
$query = mysql_query($sql); // run the query using the legacy api
return $query ? new QueryAdapter($query) : false; // return a new query adapter or false.
}
}
...
// an example of a basic mock object to test sql queries being sent to the server (inspired by mockjax :-) )
class DBMock implements DBConnAdapter {
public $queries = []; // array of queries already executed
public $results = []; // array of precomputed results. (key is SQL and value is the returned result (nested array))
public function query($sql) {
if($this->results[$sql]){
$query = new DBQueryMock($sql, $this->results[$sql]); // a mock of PDOStatement that takes the sql it ran and the results to return
$queries[] = $query; // add the query to the array
return $query; // return the query
}
return false; // we do not know the statement so lets pretend it failed
}
// add a result to the list
public function add_single_result($sql, $result){
// check if the index was set, if not make an array there
if(!isset($this->results[$sql])) $this->results[$sql] = [];
// add the result to the end of the array
$this->results[$sql][] = $result;
// return its index
return count($this->results[$sql]) - 1;
}
}
Admittedly this is not an ideal solution as it requires modifying the code to support the adapter objects and removes some functionality (such as mysql_real_escape_string), but it worked....
If you have a better solution please share :-) Thanks!
Wondering if anyone knows how or of a package that allows migrations to be done on multiple databases without having the additional connection in the config file. Currently I have two different connections, one for the default and another for the tenants. The tenant connection has the credentials input, but it doesn't have the database name since each tenant will have a different database name. Problem is when I'm doing the initial setup and need the migrations to be run it will do it for the default or the tenant(if I edit the file to include the database name). I had found a package that would do it but it is not currently compatible with laravel 5. Obviously if I have to do it manually it can be done. It would just be a pain. Thanks in advance for suggestions.
Looking through the tenancy related questions here, I stumbled upon your open one.
What I've done is override the default MigrateCommand provided by Laravel and add the --tenant option. This will allow me to migrate one, more or all tenants. You can check the implementation out at my repository. Knowing the code of conduct here, I'll also add an example implementation:
<?php
namespace Hyn\MultiTenant\Commands\Migrate;
use Hyn\MultiTenant\Traits\TenantDatabaseCommandTrait;
use Illuminate\Database\Migrations\Migrator;
use PDOException;
class MigrateCommand extends \Illuminate\Database\Console\Migrations\MigrateCommand
{
use TenantDatabaseCommandTrait;
/**
* MigrateCommand constructor.
*
* #param Migrator $migrator
*/
public function __construct(Migrator $migrator)
{
parent::__construct($migrator);
$this->website = app('Hyn\MultiTenant\Contracts\WebsiteRepositoryContract');
}
public function fire()
{
// fallback to default behaviour if we're not talking about multi tenancy
if (! $this->option('tenant')) {
$this->info('No running tenancy migration, falling back on native laravel migrate command due to missing tenant option.');
return parent::fire();
}
if (! $this->option('force') && ! $this->confirmToProceed()) {
$this->error('Stopped no confirmation and not forced.');
return;
}
$websites = $this->getWebsitesFromOption();
// forces database to tenant
if (! $this->option('database')) {
$this->input->setOption('database', 'tenant');
}
foreach ($websites as $website) {
$this->info("Migrating for {$website->id}: {$website->present()->name}");
$website->database->setCurrent();
$this->prepareDatabase($website->database->name);
// The pretend option can be used for "simulating" the migration and grabbing
// the SQL queries that would fire if the migration were to be run against
// a database for real, which is helpful for double checking migrations.
$pretend = $this->input->getOption('pretend');
// Next, we will check to see if a path option has been defined. If it has
// we will use the path relative to the root of this installation folder
// so that migrations may be run for any path within the applications.
if (! is_null($path = $this->input->getOption('path'))) {
$path = $this->laravel->basePath().'/'.$path;
} else {
$path = $this->getMigrationPath();
}
try {
$this->migrator->run($path, $pretend);
} catch (PDOException $e) {
if (str_contains($e->getMessage(), ['Base table or view already exists'])) {
$this->comment("Migration failed for existing table; probably a system migration: {$e->getMessage()}");
continue;
}
}
// Once the migrator has run we will grab the note output and send it out to
// the console screen, since the migrator itself functions without having
// any instances of the OutputInterface contract passed into the class.
foreach ($this->migrator->getNotes() as $note) {
$this->output->writeln($note);
}
}
// Finally, if the "seed" option has been given, we will re-run the database
// seed task to re-populate the database, which is convenient when adding
// a migration and a seed at the same time, as it is only this command.
if ($this->input->getOption('seed')) {
$this->call('db:seed', ['--force' => true, '--tenant' => $this->option('tenant')]);
}
}
/**
* Prepare the migration database for running.
*
* #return void
*/
protected function prepareDatabase($connection = null)
{
if (! $connection) {
$connection = $this->option('database');
}
$this->migrator->setConnection($connection);
if (! $this->migrator->repositoryExists()) {
$options = ['--database' => $connection];
$this->call('migrate:install', $options);
}
}
/**
* #return array
*/
protected function getOptions()
{
return array_merge(
parent::getOptions(),
$this->getTenantOption()
);
}
}
If you have any questions to solve this issue, let me know.
my Code
function db_fetch_array($query_id = '')
{
if(!$query_id)
{
$query_id = $this->query_result;
}
if($query_id)
{
// success
$this->row[$query_id] = mysql_fetch_array($query_id);
return $this->row[$query_id];
}
else
{
// failure
return false;
}
}
for starters i would do something like this:
if (empty($query_id))
instead of
if (!$query_id)
since !$query_id does not check for empty strings
Further, the error seems to be an issue with using the $query_id as an array key. You may need to find a different identifier
as per divaka's potential alternative in comments below:
Use something else for a key. You can fetch the result first, then get the id of the row and apply it as a key in you array
In case you want to stack several database connections,
consider linking the database in the constructor and instanciate several of these classes, which you can hold in an array.
Example:
class Database
{
protected $link;
public function __construct($user, $pass, $host) {
$this->link = new Mysqli(/* ... */);
}
public function fetch($query) {
$stmt = $this->link->prepare($query);
/* ... */
}
/* ... */
}
Now have your databases in an array:
$db[0] = new Database('root', 'root', 'server1');
$db[1] = new Database('root', 'root', 'server2');
$db['bigCluster'] = new Database('user01', 'passwd', 'cluster.your.biz');
$result = $db[0]->fetch("SELECT * FROM table");
This way you have way better access, control and debugging is easier.
To say something about your code:
function db_fetch_array($query_id = '') { /* .... */ }
This function accepts an empty parameter, thus, allowing the user to fetch from a non-existent database connection. Which results in an error. Don't allow it!
Even better, typehint it:
public function db_fetch_array(\Mysqli $query_id) { /* .... */ }
Solves all your problems at once!
If you need an it your way tho, try the following, giving you a unique, printable id to use as an array key:
$query_id = md5((string)$query_id);
Have a look at the Zend code style guide and modern PHP design patterns, this will help you to increase your code quality by a few hundred percent.
I have been reading several posts plus the official guides about how to connect more than one database in CI. I currently connect to the default application database using the standard database.php configuration and load the other database on the fly when needed. The main purpose of this part of the app is to have an "import" feature where the users user inputs the foreign database connection data on the fly when requested.
As long as the second database connection data is correctly set, the app works like a breeze. When there's an error in the connection config I didn't find a working method to evaluate that a connection could not be estabilished to the other database.
I found out that I could check if $db->conn_id is false to eventually return an error to the user but for some reasons it returns an object no matter what.
This is a brief example of what I'm doing inside the model:
function plugin_subscribers_import_sfguard($channel_id)
{
// Get the channel object by its ID
$channel = $this->get_channel_by('id',$channel_id);
// Set the preferences for the second database
// from the channel informations we retrieved
$db['hostname'] = $channel->host;
$db['username'] = $channel->user;
$db['password'] = $channel->password;
$db['database'] = $channel->db;
$db['dbdriver'] = "mysql";
$db['pconnect'] = FALSE;
$db['db_debug'] = TRUE;
// Open the connection to the 2nd database
$other_db = $this->load->database($db,TRUE);
if ( ! $other_db->conn_id )
{
// This never gets executed as $other_db->conn_id always
// returns: "resource(34) of type (mysql link)"
return false;
}
else
{
// Execute the rest of the import script
$other_db->select('*');
$other_db->from('sf_guard_user');
$other_db->join('sf_guard_user_profile',
'sf_guard_user_profile.id=sf_guard_user.id',
'inner');
$query = $other_db->get();
}
}
I wonder if there's something I didn't get out of the whole thing or if I'm using the wrong logic to evaluate if the secondary database has a proper connection open.
I also tried to try/catch the connection issue with no success.
Thanks in advance for all the support you can offer.
Federico
It's because by setting the second parameter to TRUE (boolean) the function will return the database object and in the DB.php there is a function DB and the last code block is
function &DB($params = '', $active_record_override = NULL)
{
// ...
$driver = 'CI_DB_'.$params['dbdriver'].'_driver';
$DB = new $driver($params);
if ($DB->autoinit == TRUE)
{
$DB->initialize();
}
if (isset($params['stricton']) && $params['stricton'] == TRUE)
{
$DB->query('SET SESSION sql_mode="STRICT_ALL_TABLES"');
}
return $DB;
}
So, I think, if you call this
$other_db = $this->load->database($db,TRUE);
wiuthout the TRUE
$other_db = $this->load->database($db);
Then it could give you a different result.
Update : if i you want to use
$other_db = $this->load->database($db,TRUE);
then you can also check for a method availability using method_exists function, like
$other_db = $this->load->database($db,TRUE);
if( method_exists( $other_db, 'method_name' ) ) {
// ...
}
I'm trying to access an FTP server from my PHP script using Codeigniter's FTP Library. These functions work great, but when testing the script I discovered that if I attempt to connect to a server that does not exist, the script does not terminate with an error message of any kind.
The page continues to execute, until the web server gives up, returning an empty document.
So I am wondering, is there a way to limit the amount of time that Codeigniter can try to connect to an FTP server, then display a message if that times out?
I tried using the php function set_time_limit(), but it does not behave how I expected it to.
Thanks for your help.
Codeigniter's ftp class uses the underlying ftp_connect php call that supports a 3rd optional parameter, timeout (http://ca2.php.net/manual/en/function.ftp-connect.php).
Codeigniter however does not use it, but allows for extending the default libraries it provides (providing that you're willing to do some work and check that any updates you do to the core will not break the functionality of your extended class). So to solve your problem you could create a new library in you application library folder:
<?php
class MY_FTP extends CI_FTP { //Assuming that in your config.php file, your subclass prefix is set to 'MY_' like so: $config['subclass_prefix'] = 'MY_';
var $timeout = 90;
/**
* FTP Connect
*
* #access public
* #param array the connection values
* #return bool
*/
function connect($config = array())
{
if (count($config) > 0)
{
$this->initialize($config);
}
if (FALSE === ($this->conn_id = ftp_connect($this->hostname, $this->port, $this->timeout)))
{
if ($this->debug == TRUE)
{
$this->_error('ftp_unable_to_connect');
}
return FALSE;
}
if ( ! $this->_login())
{
if ($this->debug == TRUE)
{
$this->_error('ftp_unable_to_login');
}
return FALSE;
}
// Set passive mode if needed
if ($this->passive == TRUE)
{
ftp_pasv($this->conn_id, TRUE);
}
return TRUE;
}
}
?>
and from your script, you could add to your configuration array the timeout option:
$this->load->library('ftp'); //if ftp is not autoloaded
$ftp_params = array('hostname'=>'1.2.3.4', 'port'=>21, 'timeout'=>10); //timout is 10 seconds instead of default 90
$ftp_conn = $this->ftp->connect($ftp_params);
if(FALSE === $ftp_conn) {
//Code to handle error
}
The ftp class is not designed to give error messages unless the debug parameter is set to TRUE in te config array, in which case it'll just display an error. However it can also be override, because all errors call the function _error() in the class. So you could set 'debug' => true in your $ftp_params array, and add a function in MY_ftp like so:
/**
* This function overrides
*/
function _error($line)
{
$this->error = $line;
}
And then have a function getError()
/**
* This function overrides
*/
function get_error()
{
return $this->error;
}
So if
$ftp_conn = $this->ftp->connect($ftp_params);
returns false, you can call
$error = $this->ftp->get_error();
to get your error and display it.
Now, you can always customize and have a more complex error handling mechanism by further customizing the class...
Hope it answers your question.
The answer is simple, don't attempt to connect to a server that doesn't exist.