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.
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)
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'm learning Symfony2 with Doctrine, so i'm new at it.
This is my issue: I've got Users table with 'statusId' column (just example, much more like that in my project). I also have DictStatus table with 'id' and 'name' columns (id => name == key => value). Is it possible in doctrine2 to add some constants (like: const ACTIVE = 1;) to my DictStatus mapping, so that it would be automatically inserted or updated in database as row with id='1' and name='ACTIVE'?
If that would be impossible could I extract constants with http://php.net/manual/en/reflectionclass.getconstants.php, prepare inserts and run my script automatically with
doctrine:schema:update --force
Or what about not using DictTable and keeping my Statuses only hardcoded as constants? Would that be unelegant or sopmething ;) ?
If the status is just a scalar value, I would recommend not to create a separate entity for it. This will save you a huge amount of DB queries later. The most efficient way would be to handle it as integer.
If you feel safer with constants, you can implement them as properties of the entity class.
<?php
namespace Your\SomethingBundle\Entity;
use \Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Foobar
{
const STATUS_GREAT = 1;
const STATUS_NOTSOGREAT = 0;
/**
* #ORM\Column(type="integer")
*/
protected $status;
public function setStatus($status)
{
$this->status = $status;
}
}
Usage example:
$myFoobar = new Foobar();
$myFoobar->setStatus(Foobar::STATUS_GREAT);
Validation of the $status value can be done in the setter itself or via a Validator annotation.
I have an model with a relation, and I want to instantiate a new object of the relations type.
Example: A person has a company, and I have a person-object: now I
want to create a company-object.
The class of the companyobject is defined in the relation, so I don't think I should need to 'know' that class, but I should be able to ask the person-object to provide me with a new instance of type company? But I don't know how.
This is -I think- the same question as New model object through an association , but I'm using PHPActiveRecord, and not the ruby one.
Reason behind this: I have an abstract superclass person, and two children have their own relation with a type of company object. I need to be able to instantiate the correct class in the abstract person.
A workaround is to get it directly from the static $has_one array:
$class = $this::$has_one[1]['class_name'];
$company = new $class;
the hardcoded number can of course be eliminated by searching for the association-name in the array, but that's still quite ugly.
If there is anyone who knows how this is implemented in Ruby, and how the phpactiverecord implementation differs, I might get some Ideas from there?
Some testing has revealed that although the "search my classname in an array" looks kinda weird, it does not have any impact on performance, and in use it is functional enough.
You can also use build_association() in the relationship classes.
Simplest way to use it is through the Model's __call, i.e. if your relation is something like $person->company, then you could instantiate the company with $company = $person->build_company()
Note that this will NOT also make the "connection" between your objects ($person->company will not be set).
Alternatively, instead of build_company(), you can use create_company(), which will save a new record and link it to $person
In PHPActiveRecord, you have access to the relations array. The relation should have a name an you NEED TO KNOW THE NAME OF THE RELATIONSHIP/ASSOCIATION YOU WANT. It doesn't need to be the classname, but the classname of the Model you're relating to should be explicitly indicated in the relation. Just a basic example without error checking or gritty relationship db details like linking table or foreign key column name:
class Person extends ActiveRecord\Model {
static $belongs_to = array(
array('company',
'class_name' => 'SomeCompanyClass')
);
//general function get a classname from a relationship
public static function getClassNameFromRelationship($relationshipName)
foreach(self::$belongs_to as $relationship){
//the first element in all relationships is it's name
if($relationship[0] == $relationshipName){
$className = null;
if(isset($relationship['class_name'])){
$className = $relationship['class_name'];
}else{
// if no classname specified explicitly,
// assume the clasename is the relationship name
// with first letter capitalized
$className = ucfirst($relationship);
}
return $className
}
}
return null;
}
}
To with this function, if you have a person object and want an object defined by the 'company' relationship use:
$className = $person::getClassNameFromRelationship('company');
$company = new $className();
I'm currently using below solution. It's an actual solution, instead
of the $has_one[1] hack I mentioned in the question. If there is a
method in phpactiverecord I'm going to feel very silly exposing
msyelf. But please, prove me silly so I don't need to use this
solution :D
I am silly. Below functionality is implemented by the create_associationname call, as answered by #Bogdan_D
Two functions are added. You should probably add them in the \ActiveRecord\Model class. In my case there is a class between our classes and that model that contains extra functionality like this, so I put it there.
These are the 2 functions:
public function findClassByAssociation($associationName)
Called with the name of the association you are looking for.
Checks three static vars (has_many,belongs_to and has_one) for the association
calls findClassFromArray if an association is found.
from the person/company example: $person->findClassByAssociation('company');
private function findClassFromArray($associationName,$associationArray)
Just a worker-function that tries to match the name.
Source:
/**
* Find the classname of an explicitly defined
* association (has_one, has_many, belongs_to).
* Unsure if this works for standard associations
* without specific mention of the class_name, but I suppose it doesn't!
* #todo Check if works without an explicitly set 'class_name', if not: is this even possible (namespacing?)
* #todo Support for 'through' associations.
* #param String $associationName the association you want to find the class for
* #return mixed String|false if an association is found, return the class name (with namespace!), else return false
* #see findClassFromArray
*/
public function findClassByAssociation($associationName){
//$class = $this::$has_one[1]['class_name'];
$that = get_called_class();
if(isset($that::$has_many)){
$cl = $this->findClassFromArray($associationName,$that::$has_many);
if($cl){return $cl;}
}
if(isset($that::$belongs_to)){
$cl = $this->findClassFromArray($associationName,$that::$belongs_to);
if($cl){return $cl;}
}
if(isset($that::$has_one)){
$cl = $this->findClassFromArray($associationName,$that::$has_one);
if($cl){return $cl;}
}
return false;
}
/**
* Find a class in a php-activerecord "association-array". It probably should have a specifically defined class name!
* #todo check if works without explicitly set 'class_name', and if not find it like standard
* #param String $associationName
* #param Array[] $associationArray phpactiverecord array with associations (like has_many)
* #return mixed String|false if an association is found, return the class name, else return false
* #see findClassFromArray
*/
private function findClassFromArray($associationName,$associationArray){
if(is_array($associationArray)){
foreach($associationArray as $association){
if($association['0'] === $associationName){
return $association['class_name'];
}
}
}
return false;
}
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.