I'm referring to using the die() function for something else than debugging.
This is a "well it works" situation, but is it bad practice?
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Filesystem\Exception\IOExceptionInterface;
/**
* This Command will help Cjw create a new demo site to start off
* with the Multisite bundle.
*
* Class CreateSiteCommand
* #package Cjw\GeneratorBundle\Command
*/
class RemoveSiteCommand extends ContainerAwareCommand
{
private $vendor; //eg "Cjw"
private $fullBundleName; //eg "SiteCjwNetworkBundle"
private $fullBundleNameWithVendor; //eg "CjwSiteCjwNetworkBundle"
(more vars)
/**
* this function is used to configure the Symfony2 console commands
*/
protected function configure()
{
$this
->setName('cjw:delete-site')
->setDescription('Delete Cjw Site')
->addOption('db_user', null, InputOption::VALUE_REQUIRED, 'If set, the database user will be shown on the instructions');
}
/**
* This function executes the code after the command input has been validated.
*
* #param InputInterface $input gets the user input
* #param OutputInterface $output shows a message on the command line
* #return int|null|void
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// set optional db_username
$dialog = $this->getHelper('dialog');
$reusable = new \Reusable();
$fs = new Filesystem();
$cmd = new \ColorOutput();
//push only vendors into the vendors array
$vendors = $reusable->getFileList($reusable->getMainDirectory('src'),true);
//select a vendor from the array
$vendor = $dialog->select(
$output,
'Select your vendor [1]: ',
$vendors,
1
);
$bundles_in_vendor = $reusable->getFileList($reusable->getMainDirectory('src/'.$vendors[$vendor]),true);
//push bundles that start with 'Site' into array
$sites = array();
foreach($bundles_in_vendor as $bundle)
{
if($reusable->startsWith($bundle,'Site'))
{
array_push($sites,$bundle);
}
}
$site_to_delete = $dialog->select(
$output,
'Select site to remove: ',
$sites,
1
);
$bundle_deletion_path = $reusable->getMainDirectory('src/'.$vendors[$vendor]).'/'.$sites[$site_to_delete];
$are_you_sure = array('yes','no');
$confirmation = $dialog->select(
$output,
'Are you sure you want to delete: '.$sites[$site_to_delete],
$are_you_sure,
1
);
if($are_you_sure[$confirmation] == 'yes')
{
echo $cmd->color('yellow','attempting to remove bundle in: '.$bundle_deletion_path);
$fs->remove($bundle_deletion_path);
//returns demo
$sitename = strtolower($sites[$site_to_delete]);
$sitename = substr($sitename,0,-6);
$sitename = substr($sitename,4);
$this->setRawSiteNameInput($sitename);
// returns acmedemo
$symlinkname = strtolower($vendors[$vendor].substr($sites[$site_to_delete],0,-6));
$this->removeSymlinks($symlinkname,$this->getRawSiteNameInput());
$this->createSetters($vendor,substr($sites[$site_to_delete],0,-6));
$this->deleteEzPublishExtension();
echo $this->getFullLegacyPath();
echo $cmd->color('green','deletion process completed.');
}
else
{
echo "deletion canceled";
die();
}
function_that_further_deletion_process();
}
This is a symfony2 console script that removes a site from a certain structure
That is perfectly safe, if that is your question, since php as a quasi interpreted language does not leave any traces or artifacts when terminating the execution.
If it is a good practice is another thing. I'd say it is fine for testing purposes, but you should avoid it in final code. Reason is that it makes code hard to maintain. Consider someone else diving into your code and trying to understand the logic. One would actually have to go through all the code to stumble over this detail. Chances is one does not, so the behavior of your code appears broken.
Instead try to do one of these:
throw an exception to leave the current scope. Such an exception might well be caught and swallowed by the calling scope, but it is a clear and predictable way of returning. Obviously you should document such behavior.
return a value clearly out of scope to what would "normally" be returned. So for example null or false instead of a typical value. This forces the calling scope to check the return value, but that is good practice anyway.
restructure your code such that there is no reason to suddenly terminate the execution.
You haven't told us what your intention was with that die.. consequently we cannot tell whether you're using the right tool...
die and it's synonym, exit both exit the script. If that's what you want, it's fine.
I tend to use exit for normal operations, and die for erroneous situations (something you cannot properly recover from, e.g.: couldn't connect to database). I also use a wrapper for die so if needed, I can log such events. Though both commands do the same thing, there's a difference in intention, and I want to express that intention in the code.
In your example I would use exit.
Related
Background
I have a system with a microservices setup. A few of these microservices run a laravel installation. In order to share some key models, a repo was shared using git/packagist.
Here is a diagram:
Microservice A
Microservice B
...
These both share Library C. This library has the shared models. This is outside of a normal laravel installation, but the composer includes "laravel/framework": "^9.0".
Note: There good external reasons to share the functionality - the microservices have come out of a monolith and are still developing fluidly and are not mature enough for a complete decoupling. This will come in time.
I wish to unit test these models.
Specifics
The requirement is that several models (User, Customer .. etc) all require addresses. Normalising these out would introduce complexity elsewhere that is not appropriate yet, so a trait is good for now. These have UK postcodes that require a specific validation against a database. Postcodes are modelled using a Postcode model.
I created a trait : AddressTrait. This offers some useful functionality. Included in this is a Postcode validation. This intercepts a set request in laravel (eg: $user->postcode = 'AB10 1AB)
/**
* Automatically updates the log/lat from the postcode
* #param $value
*/
public function setPostcodeAttribute($value): void
{
// update postcode
$this->attributes['postcode'] = strtoupper($value);
// now update lat/long
$postcode = Postcode::where('pcd', '=', str_replace(' ', '', $value))
->orWhere('pcd', '=', $value)
->first();
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
This works as expected.
Note - it is to be extended quite a bit further with much more complexity, but this is step 1 and completely represents the problem.
Testing
If I interact with the postcode attribute, such as $user->postcode = 'AB10 1AB, this attempts to load the Postcode from the database, and the following error occurs:
Error : Call to a member function connection() on null
^ This is expected.
I would like to unit test this: ie. no reaching out the class and mocking system/functional elements. Thus, I need to mock the Postcode load (Postcode::where(..) .. ).
As this is a static call, I have used mockery ("mockery/mockery": "dev-master").
Here is the current attempt:
// ...
use Mockery;
use PHPUnit\Framework\TestCase;
// ...
public function testPostcodeProcessing(): void
{
$postcode_value = 'AB10 1AB';
$postcode_content = [
'pcd' => $postcode_value,
'latitude' => '0.1',
'longitude' => '0.2'
];
$mock_postcode = Mockery::mock(Postcode::class);
$mock_postcode->shouldReceive('where')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('orWhere')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('first')->once()->andReturn($postcode_content);
$model = $this->createTraitImplementedClass();
$model->postcode = $postcode_value;
}
protected function createTraitImplementedClass(): Model
{
return new class extends Model {
use AddressTrait;
};
}
TLDR question
I would like to unit test this function: ie. no reaching out the class and mocking.
How do I mock a laravel/eloquent static call, given that:
this is to be tested outside laravel
there is no database connection
OR
How do I refactor this to allow it to be more testable
Super TLDR;
How do I mock the load in:
public function tldr(): void
{
// this eloquent lookup needs to be mocked (not moved, refactored etc etc..)
$postcode = Postcode::where('pcd', '=', 'AB10 1AB')->first();
}
Notes:
These are unit tests
I would prefer to do this "the laravel way", but given the unusual circumstances things such as mockery might make sense
May be a gotcha: I am using the phpunit PHPUnit\Framework\TestCase - not the usual PHP test case. This is not a "requirement", but I imagined a mock shouldn't need the extended features.
Any help with this would be appreciated!
What if you abstracted away the part where you get the postcode?
public function setPostcodeAttribute($value): void
{
// update postcode
$this->attributes['postcode'] = strtoupper($value);
// now update lat/long
$postcode = $this->getPostCode($value);
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
// you could make this method protected as well
// but if you do, your need to call the shouldAllowMockingProtectedMethods()
// when creating your mock
public function getPostCode(string $value): ?Postcode
{
return Postcode::where('pcd', '=', str_replace(' ', '', $value))
->orWhere('pcd', '=', $value)
->first();
}
If you do it like this, you no longer need to mock Eloquent Query builder at all. Partially mocking a class that uses that Address trait should give you what you need. I'm not sure if this works for anonymous classes though
public function test_existing_postcode()
{
// Arrange
$userMock = Mockery::mock(User::class)->makePartial();
$user = new User;
$postcode_value = 'AB10 1AB';
$postcode = new PostCode([
'pcd' => $postcode_value,
'latitude' => '0.1',
'longitude' => '0.2'
]);
// Expect
$userMock->expects()
->getPostCode($postcode_value)
->andReturn($postcode);
// Act
$user->postcode = $postcode_value;
// Assert
$this->assertEquals($user->latitude, $postcode->latitude);
$this->assertEquals($user->longitude, $postcode->longitude);
}
public function test_nonexisting_postcode()
{
// Arrange
$userMock = Mockery::mock(User::class)->makePartial();
$user = new User;
$postcode_value = 'AB10 1AB';
// Expect
$userMock->expects()
->getPostCode($postcode_value)
->andReturn(null);
// Act
$user->postcode = $postcode_value;
// Assert
$this->assertNull($user->latitude);
$this->assertNull($user->longitude);
}
Although I wouldn't recommend it, if you had a static method inside the Postcode model.
class Postcode extends Model
{
public static function getPostcodeByValue(string $value): ?Postcode
{
return Postcode::...
}
}
You could mock it with
$postcodeMock = \Mockery::mock('alias:Postcode');
$postcodeMock->shouldReceive('getPostcodeByValue')
->with($value)
->andReturn($postcode);
I'm not sure if expects() works, but if it does, you can also write this as
$postcodeMock = \Mockery::mock('alias:Postcode');
$postcodeMock->expects()
->getPostcodeByValue($value)
->andReturn($postcode);
Important: for this to work, the Postcode class should not have been loaded (by this or any previous tests). It's that fragile.
You can make your method more test friendly
Injectable external class to remove hidden dependencies
Keep the formatting/input validation outside if it is not related to "something" structural
Separate functionalities or the S in SOLID principles (move the lookup for Postcode instance to where it belongs)
like this
/**
* Automatically updates the log/lat from the postcode
* #param string $value
* #param Postcode $postcode
*/
public function setPostcodeAttribute($value, Postcode $postcode = null): void
{
// update postcode
$this->attributes['postcode'] = $value;
if ($postcode) {
$this->attributes['latitude'] = $postcode->latitude;
$this->attributes['longitude'] = $postcode->longitude;
}
}
After some extensive looking into this, I've found the answer using mockery aliases. This is done as follows:
Isolate this class/test from the remainder of the tests
If you create an alias, this overwrites the class globally for the rest of the current process. It's risky, but this can be done and many of the problems sidestepped by running the test/class in a separate process.
This can be done using the docblock:
/**
* At a class level
* #runTestsInSeparateProcesses
* #preserveGlobalState disabled
*/
Mock the class as an alias
Aliases mock static classes. This is the key point I was missing during my question - I missed the alias: part.
public function testPostcodeProcessing(): void
{
// define this first to intercept the global instantiation
$mock_postcode = Mockery::mock('alias:' . Postcode::class);
// ...
}
The above mock will override ALL Postcode classes in this test/test class. Thus, it should be declared first.
Add your responses and assertions
This is entirely up to you, but here is the example and assertions I created.
/*
* Tests that the postcode processes correctly.
*/
public function testPostcodeProcessing(): void
{
// define this first to intercept the global instantiation
$mock_postcode = Mockery::mock('alias:' . Postcode::class);
// set up a returned class
$returned_postcode = new Postcode();
$postcode_pcd = 'AB10 1AB';
$postcode_latitude = 0.1;
$postcode_longitude = 0.2;
$returned_postcode->pcd = $postcode_pcd;
$returned_postcode->latitude = $postcode_latitude;
$returned_postcode->longitude = $postcode_longitude;
// Set up the mock
$mock_postcode->shouldReceive('where')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('orWhere')->once()->andReturn($mock_postcode);
$mock_postcode->shouldReceive('first')->once()->andReturn($returned_postcode);
$model = $this->createTraitImplementedClass();
$model->postcode = $postcode_pcd;
$this->assertEquals($postcode_pcd, $model->postcode, 'The postcode object pcd was not set');
$this->assertEquals($postcode_latitude, $model->latitude, 'The postcode object latitude was not loaded');
$this->assertEquals($postcode_longitude, $model->longitude, 'The postcode object longitude was not loaded');
}
Note - these are "step 1" tests. The real class is more complex, and the test will be more complex. However, this gives the core solution to the instantiation issue.
TLDR;
Run this in a separate process
Use an Alias (and remember to declare it as an alias - alias:SomeClass)
I am writing a Product Class, the job of the Class is to take in a product id and to output the corresponding product name.
For e.g.:
$Product = new Product;
$Product->id = "ff62";
$Product->readId();
echo $Product->name;
// returns a string with at least 5 characters.
My PHPUnit test method looks like:
$Product = new Product;
$Product->id = "ff62"; // needs to be a variable
$Product->readId();
$this->assertEquals(gettype($Product->name), 'string');
However, my aim is to check for a different product ID each time instead of ff62 which may or may not exist in database.
Ideally one should be able to define the id variable during testing.
What is the best way to test for dynamic variables as such?
Faker is one way to do it, but I would hesitate to say it is the "best way."
Your requirements are:
1. Test a set of different variables.
2. Those variables may or may not exist in the database.
But you have several problems with how you have designed this test:
You are using gettype() and comparing it to string. This is a bad idea. If product 54 is "foo", and your test is returning "bar" for 54, it will pass. This is Programming by Coincidence. I.e., it works, but not on purpose.
The way you're setting this up does not really deal with the problem. While Faker can create fake data, it cannot automatically create known good and known bad data for your specific system and business cases. I would assume that you want to test known good data + expected results as well as known bad data + expected exceptions.
The proper way to structure this test is using #dataProvider and database fixtures / testing.
Here's what that would look like:
<?php
namespace Foo\Bar;
use PHPUnit\DbUnit\TestCaseTrait;
use PHPUnit\Framework\TestCase;
use \PDO;
USE \Exception;
class ProductTest extends TestCase
{
use TestCaseTrait;
// only instantiate pdo once for test clean-up/fixture load
static private $pdo = null;
// only instantiate PHPUnit_Extensions_Database_DB_IDatabaseConnection once per test
private $conn = null;
final public function getConnection()
{
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
}
return $this->conn;
}
public function getDataSet()
{
return $this->createMySQLXMLDataSet('tests/unit/testdata/sampleproductdata.xml');
}
/**
* Tests products against known good data in the database fixture.
* #param $id
* #param $expectedName
* #dataProvider providerTestProduct
*/
public function testProduct($id, $expectedName) {
$Product = new Product;
$Product->id = $id;
$Product->readId();
$this->assertSame($expectedName, $Product->name);
}
/**
* Provides data that should appear in the database.
* #return array
*/
public function providerTestProduct() {
// id , expectedName
return [ [ "ff62" , "fooproduct"]
, [ "dd83" , "barproduct"]
, [ "ls98" , "bazproduct"]
];
}
/**
* Tests products against known-bad data to ensure proper exceptions are thrown.
* #param $id
* #param $expectedName
*/
public function testProductExceptions($id, $expectedName) {
$Product = new Product;
$Product->id = $id;
$this->expectException(Exception::class);
$Product->readId();
}
/**
* Provides test data that when queried against the database should produce an error.
* #return array
*/
public function providerTestProductExceptions() {
// id , expectedName
return [ [ "badtype" , "fooproduct"] //Wrong id type
, [ "aaaa" , "barproduct"] //Does not exist
, [ null , "bazproduct"] //null is a no-no.
];
}
}
Here's a breakdown:
Use namespaces. Because it's 2018, and it's the right thing to do.
Use use to declare what classes you're using in the test.
Use TestCaseTrait to properly setup your TestCase
The private $pdo variable will hold your database connection for your class / test.
getConnection() is required. This will use the database, username, and password you have configured in your phpunit.xml file. Reference
getDataSet() goes and reads your datasource (fixture), then, truncates your database on your workstation / dev box, imports all the data from the fixture to put the database in a known state. (Be sure to backup your data before you do this. It's lossy on purpose. Never execute on production).
Next, you have two pairs of methods for the test cases: a test and a data provider.
The data provider in each case provides an ID you want to test, and the expected result. In the case of testProduct and providerTestProduct, we are providing ID that should exist in the database (as ensured by the fixture above). We can then check that Product::readId() is not only returning a string, but is actually returning the correct string.
In the second case, testProductException() and providerTestProductException(), we are intentionally sending bad values to the class to trigger exceptions, and then checking to make sure those bad values actually produces the desired behavior: failure / thrown exceptions.
You can randomise your dataset using random number generation.
$value = dechex(random_int(0, 255)).dechex(random_int(0, 255));
$Product = new Product;
$Product->id = $value;
$Product->readId();
$this->assertEquals('string', gettype($Product->name));
$this->assertEquals($value, $Product->name);
One usually puts the expected value to the left, and the actual one to the right.
I found out the best way to do this is to use Faker.
https://github.com/fzaninotto/Faker
While I was trying to test against different instances of a Product, I could definitely use Faker to randomly generate a product and test if the Product was being retrieved properly from the database.
Although majorly used in Laravel, Symfony, etc. It's quite easy to use even in custom PHP frameworks.
What can I use other than if(!empty( $product->a_funky_function() )) to check if the method is empty before calling it?
I've tried method_exists() and function_exists() and a whole plethora of conditions. I think the issue is that I need to have my $product variable there.
Please help.
A fairly common pattern is to have two methods on your class, along the lines of getField and hasField. The former returns the value, and the latter returns true or false depending whether or not the value is set (where "set" can mean not null, or not empty, or whatever else you might want it to mean).
An example:
class Foo
{
/** #var string */
private $field;
/**
* #return string
*/
public function getField()
{
return $this->field;
}
/**
* #return bool
*/
public function hasField()
{
return $this->getField() !== null;
}
}
This would then be used like:
if ($foo->hasField()) {
$field = $foo->getField();
...
}
Often, like in this example, the has... method often just delegates to the get... method internally, to save duplicating the logic. If the getter contains particularly heavy processing (e.g. a database lookup or API call), you might want to factor in ways to avoid performing it twice, but that's a bit out of scope.
You need absolutely to call it so it can compute the value which it will return, so I think there is no other way to know if it will return something not empty without calling it...
You have to call it.
$result = $product->getFunction();
if(!empty($result)) {
//your code here
}
It's the first time I run into this problem. I want to create a doctrine object and pass it along without having to flush it.
Right after it's creation, I can display some value in the object, but I can't access nested object:
$em->persist($filter);
print_r($filter->getDescription() . "\n");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 0
(I should have 19 assetClass)
If I flush $filter, i still have the same issue (why oh why !)
The solution is to refresh it:
$em->persist($filter);
$em->flush();
$em->refresh($filter);
print_r($filter->getDescription() . " -- ");
print_r(count($filter->getAssetClasses()));
die;
I get:
filter description -- 19
unfortunately, you can't refresh without flushing.
On my entities, I've got the following:
in class Filter:
public function __construct()
{
$this->filterAssetClasses = new ArrayCollection();
$this->assetClasses = new ArrayCollection();
}
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(\App\CoreBundle\Entity\FilterAssetClass $filterAssetClass)
{
$this->filterAssetClasses[] = $filterAssetClass;
$filterAssetClass->setFilter($this);
return $this;
}
in class FilterAssetClass:
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="App\CoreBundle\Entity\Filter", inversedBy="filterAssetClasses")
*/
private $filter;
/**
* #var Filter
*
* #ORM\ManyToOne(targetEntity="AssetClass")
*/
private $assetClass;
public function setFilter(\App\CoreBundle\Entity\Filter $filter)
{
$this->filter = $filter;
return $this;
}
Someone else did write the code for the entities, and i'm a bit lost. I'm not a Doctrine expert, so if someone could point me in the good direction, that would be awesome.
Julien
but I can't access nested object
Did you set those assetClasses in the first place?
When you work with objects in memory (before persist), you can add and set all nested objects, and use those while still in memory.
My guess is that you believe that you need to store objects to database in order for them to get their IDs assigned.
IMHO, that is a bad practice and often causes problems. You can use ramsey/uuid library instead, and set IDs in Entity constructor:
public function __construct() {
$this->id = Uuid::uuid4();
}
A database should be used only as a means for storing data. No business logic should be there.
I would recommend this video on Doctrine good practices, and about the above mentioned stuff.
Your problem is not related to doctrine nor the persist/flush/refresh sequence; the problem you describe is only a symptom of bad code. As others have suggested, you should not be relying on the database to get at your data model. You should be able to get what you are after entirely without using the database; the database only stores the data when you are done with it.
Your Filter class should include some code that manages this:
// Filter
public function __contsruct()
{
$this->filterAssetClasses = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="FilterAssetClass", mappedBy="filterAssetClasses", cascade={"persist"})
*/
private $filterAssetClasses;
public function addFilterAssetClass(FilterAssetClass $class)
{
// assuming you don't want duplicates...
if ($this->filterAssetClasses->contains($class) {
return;
}
$this->filterAssetClasses[] = $class;
// you also need to set the owning side of this relationship
// for later persistence in the db
// Of course you'll need to create the referenced function in your
// FilterAssetClass entity
$class->addFilter($this);
}
You may have all of this already, but you didn't show enough of your code to know. Note that you should probably not have a function setFilterAssetClass() in your Filter entity.
I have a simple PHP / MySql application which will generally pick one of several databases (let's say one per customer) to manipulate. However, there are frequent calls to utility functions which access a common database.
I don't want to sprinkle USE clauses throughout my code, so it looks like I ought to push the current database at the start of each utility function and pop it again at the end. Something like this (from the top of my head, so prolly won't work, but will give an idea).
function ConnectToDatabase($db)
{
global $current_database;
$current_database = $db;
odb_exec('USE ' . $db); // etc. Error handling omitted for clarity
}
function UtilityFunction()
{
odb_exec('USE common_db'); // etc. Error handling omitted for clarity
// do some work here
global $current_database;
ConnectToDatabase($current_database);
}
Maybe I can make it prettier by combining global $current_database; ConnectToDatabase($current_database); into a PopCurrentDb function, but you get the picture.
is this better done in PHP? Is there a MySql solution (but later I want to be ODBC compliant, so maybe PHP is better). How do others do it?
Update: in the end I just decided to always fully qualify access,
e.g. SELECT * from $database . '.' . $table
Why dont you just make some kind of database manager class and just push that around? Centralize all you dbname/connection storage in a single entity. that way you have a clear api to access it and you can just use the db by name.
class MultiDb
{
/*
* Array of PDO DB objects or PDO DSN strings indexed by a connection/dbname name
*
* #var array
*/
protected $connections = array();
/*
* The connection name currently in use
* #var string
*/
protected $currentConnection;
/*
* The Defualt connection name
*
* #var string
*/
protected $defaultConncetion;
/*
* #param array $connections Any array DSN or PDO objects
*/
public function __construct(array $connections);
public function getConnection($name);
// i would set this up to intelligently return registered connections
// if the argument matches one
public function __get($name)
// same with __set as with __get
public function __set($name, $value);
// proxy to the current connection automagically
// if current isnt set yet then use default so things
// running through this would actually result in
// call_user_func_array(array(PDO $object, $method), $args);
public function __call($method, $args);
}
So usage might look like
// at the beginning of the app
$db = new MultiDb(array(
'util' => array('mysql:host=localhost;dbname=util;', 'user', 'pass');
'default' => array('odbc:DSN=MYDSN;UID=user;PWD=pass;');
));
// some where else in the app we want to get some ids of some entities and then
// we want to delete the associated logs in our shared utility DB
// fetch the ids from the default db
$ids = $db->default->query('SELECT c.name, c.id FROM some_table c')
->fetchAll(PDO::FETCH_KEY_PAIR);
// assume we have written a method
// to help us create WHERE IN clauses and other things
$in = $db->createQueryPart($ids, MultiDb::Q_WHERE_IN);
// prepare our delete from the utility DB
$stmt = $db->util->prepare(
'DELETE FROM log_table WHERE id IN('.$in['placeholder'].')',
$in['params']
);
// execute our deletion
$stmt->execute();
So you want to create a function to push (insert) and pop (select & remove)?
You could create a stored procedure to handle this or you can write multiple query executions in php.