I've created this class that fetches all data of a post from database.
class Post {
private $id;
protected $conn;
public $data;
function __construct(\mysqli $conn) {
$this->conn = $conn;
}
public function getId() {
return $this->id;
}
public function getConnection() {
return $this->conn;
}
public function getPost() {
$query1 = $this->getConnection()->query("SELECT * FROM posts WHERE id=" . $this->id);
if ($query1->num_rows == 1) {
$this->data = $query1->fetch_object();
return $this->data;
}
}
public function setId($id) {
$this->id = (int)$id;
}
}
Along with post, I also need to fetch all data of the user who created the post. I've three ways to do this:
1) By calling the User class inside of the Post class.
public function getPost() {
$query1 = $this->getConnection()->query("SELECT * FROM posts WHERE id=" . $this->id);
if ($query1->num_rows == 1) {
$this->data = $query1->fetch_object();
// Initiating User class
$user = new User($this->getConnection());
$user->setUserId($this->data->user_id);
$this->data->user = $user->getUserInfo();
return $this->data;
}
}
2) By extending the Post class with the User class.
class Post extends User {
....
Then calling methods from the User class
public function getPost() {
$query1 = $this->getConnection()->query("SELECT * FROM posts WHERE id=" . $this->id);
if ($query1->num_rows == 1) {
$this->data = $query1->fetch_object();
// Calling methods from the User class
$this->setUserId($this->data->user_id);
$this->data->user = $this->getUserInfo();
return $this->data;
}
}
3) By creating a User trait and using it in Post class.
class Post {
use UserTrait;
....
Then calling methods from the User trait
public function getPost() {
$query1 = $this->getConnection()->query("SELECT * FROM posts WHERE id=" . $this->id);
if ($query1->num_rows == 1) {
$this->data = $query1->fetch_object();
// Calling methods from the User trait
$this->setUserId($this->data->user_id);
$this->data->user = $this->getUserInfo();
return $this->data;
}
}
Between these 3, which one is the best approach in terms of dependency injection, performance, and cleanliness of code?
This question might get downvoted to hell because you're requesting an opinion, but I'll throw in my two cents.
You're basically building your own basic ORM. In that ORM you're dealing with entities. Different entities should be handled by different classes (Models), and there should be a 1:1 equivalence between the underlying DB table and the Model.
There are relations between your entities. Yous Post entity has an author that should be an instance of the User entity. However, you wouldn't say that the relation between the post table and the user table means they could be represented by the same Model, or different flavors of a parent Model.
If you used a full ORM (Doctrine, Propel, RedbeanPHP) you'd see that a relation such as the Post author means that, when using methods like getById() (in your case getPost() ) the retrieved Post entity will proceed to instance every depending entity by default.
In your case, instead of having $this->data->user_id the ORM would offer you a nested $this->data->user object so you wouldn't have to deal with that by yourself.
So, if you consider it's too soon to use an ORM, the first approach is easier to mantain and migrate when the time comes.
Unrelated opinion
At this point, to get a post info you need to do
$post = new Post($conn);
$post->setId($postid);
$postdata = $post->getPost();
If you changed your constructor and added a getById() method such that
function __construct(\mysqli $conn) {
$this->conn = $conn;
return $this;
}
public function getById($id) {
$this->setId($id);
return $this->getPost();
}
you could instead retrieve the post data in one line
$postdata=(new Post($conn))->getById($postid);
It all depends on what you're going to keep doing it. With the extended class was better deal if you're going to use permanent methods of other classes. If you need only a part of another class, then just call the class and its method.
CAREFUL: An extended class behaves as a single class. Here can be a lot of collisions occur if you have the same names of methods and functions.
I think this depends on other aspects of your code. Personally I would just extend the class User as I don't think using traits is necessary in this particular case.
There is a useful article here on using Traits in PHP -
Using Traits in PHP 5.4
Related
I am novice in OOP programming in php and trying to understand and implement the dependency injection feature in my MVC project. In the following I am explaining a super simple example of the feature where I am struggling applying the dependency injection. The actual application is lot more complex than this, however, this should be enough to demonstrate the problem I am having.
I have created a Model called “user” that is responsible for managing a single user. Data handled by the class (data about a user) is also saved in the database table. The “user” class has method to load from and save/update the data to the database table. The user class can be initiated with data loaded from the database (by using user id) or load from the array supplied to the constructor.
The project deals with multiple users at a time. So, I have created a container class called “users”. This class has an array of “user” objects. However, this class also have method to load data for multiple user objects from the database (based on criteria such as all paid users), then create the object array with the data. The number of object is created is depends on the number of users returned from the database.
The following is a sample code for the classes
class user
{
private $data;
function __construct ($arData=””)
{
$this->dbTable ="user";
if(!is_array($ar))
{
if($ar!="")
{
$ar = $this->getDataFromDB($ar);
}
else
{
$ar = array();
}
}
$this->data = $ar;
}
function getDataFromDB($id_user){ … data base implementation … }
....
Other methods
....
}
class users // the container class
{
private $objUsers;
function __ construct(){
$this->objUsers = array();
}
function loadUsers($type){
$userDataArray = $this->getUsersFromDatabase($type);
foreach($useDataArray as $userData){
$this->objUsers[] = new user($userData);
}
}
function getUsersFromDatabase($userType) { …… database …… }
…… other methods …..
}
My concern is the container class (container may not be the right word to say). I want to know the best practice to create this type of container class and what is the recommend for this. In addition, this is clearly evident that this container class is tightly coupled with “user” class and cannot be tested separately. How can I implement the dependency injection for a class like this?
As I said, I don't think this is a good fit for dependency injection. And I wouldn't set it up that way just for the sake of saying it uses dependency injection.
The main reason it's not a good fit, is that a User is always a User. So you always have a concrete contract between the wrapper, Users, and the User. You can count on User having certain methods. And you don't have some weird 3rd class that your adding into these collections, it's just a collection of a known and well defined object.
That said, I would go with a more factory style wrapper, Where the User is the simpler of the 2 classes. ( note, I didn't test any of this, so just look at it like psudo code )
class users {
public function createUser( array $data = []){
if( $data['id'] ){
$User = $this->getUser( $data['id'] );
if( $User )
return $User;
}
//depending what you want you could search on other things
//like the email, (unique) and return the user.
//you could make this as complex, or as simple as you want
//it could simply create a new user, or check for an existing one first.
return new User($data); //dependency
}
public function getUser( $id ){
$stmt = $this->DB->prepare( "SELECT * FROM users WHERE id=:id" );
$stmt->FetchMode(PDO::FETCH_CLASS, 'User');
return $stmt->fetch(); //fetch right into the class
}
public function saveUser( User $User ){
//I just added this to show how you can type hint the class
// so that it only accepts Objects of the type User
}
}
class user{
protected $id;
protected $username;
protected $email;
public function __construct(array $data = []){
foreach( $data as $field=>$value){
//this could be done better, with error checking etc.
//but I just wanted to show how you can use the setters
//dynamically when constructing with an array
//this is useful because we are not hard coding the schema of User in any files
//-note- PHP method calls are case insensitive.
$this->set{$field} = $value;
}
}
public function setId( $id ){ $this->id = $id; }
public function getId(){ return $this->id; }
public function setUsername( $username ){ $this->username = $username; }
public function getUsername(){ $this->username; }
public function setEmail( $email ){ $this->email = $email; }
public function getEmail(){ return $this->email; }
}
Then you can worry about dependency injection for things like the Database. This could be represented by having the users constructor accept a PDO or Database object. Like this
class Users{
protected $DB;
public function __construct( $DB ){
$this->DB = $DB;
}
}
The Users class doesn't care about the DB credentials, or even the particular DB driver your using. To some extent it does have some coupling with the driver based on the SQL syntax, which may be specific to a particular database. If we wanted to make this a "truer" form of dependency injection we should use an ORM like Doctrine, or some kind of Query builder ( instead of PDO itself ). Then we would have another layer of abstraction between our code and the database.
If you need user to have access to users and they cant be separated extend the class.
class users {
}
class user extends users {
}
Child user can then access the parent users properties.
Following from my previous post about removing ServiceLocatorAwareInterface's from my zf2 app, i am now faced with a puzzle involving object creation when using data mappers.
The current implementation of my data mapper uses a tablegateway to find specific rows, calls the service manager to obtain a domain object, then populates and returns the full object.
public function findById($userId){
$rowset = $this->gateway->select(array('id' => $userId));
$row = $rowset->current();
if (!$row) {
throw new \DomainException("Could not find user with id of $userId in the database");
}
$user = $this->createUser($row);
return $user;
}
public function createUser($data){
$userModel = $this->getServiceManager()->get('Model\User');
$hydrator = $this->getHydrator();
if($data instanceof \ArrayObject){
$hydrator->hydrate($data->getArrayCopy(), $userModel);
}else{
$hydrator->hydrate($data, $userModel);
}
return $userModel;
}
The model needs to be called from the service manager because it has other dependencies, so calling $user = new App\Model\User() from within the mapper is not an option.
However, now i am removing instances of the servicemanager from my code, i am unsure of the best way to get the model into the mapper. The obvious answer is to pass it in the constructor and save the instance as a property of the mapper:
public function __construct(TableGateway $gateway, \App\Model\User $userModel){
$this->_gateway = $gateway;
$this->_userModel= $userModel;
}
public function createUser($data){
$userModel = $this->_userModel;
//....snip....
}
This works to a degree, but then multiple calls to createUser (such as when finding all users, for instance) over writes each instance with the last objects data (as to be expected, but not what i want)
So i need a "new" object returned each time i call createUser, but the dependency being passed into the constructor. With the model passed into the constructor I can clone the object eg.
public function createUser($data){
$userModel = clone $this->_userModel
//....snip....
}
...but something about it doesn't seem right, code smell?
You are right, it doesn't smell good.
Designing an ORM isn't easy. There is and probably always will be discussion about the way an ORM should be designed. Now, when I'm trying to understand your design I noticed you are pointing out that your models contain the data but also have "other" dependencies. This is wrong, the models containing your data should work without any layer in your application.
Entities should work without the ORM
In my opinion you should separate your business logic (dependencies) from your data. This will have many advantages:
More expressive
Easier to test
Less coupling
More flexible
Easier to refactor
For more information about how to design your ORM layer I highly recommend browsing through these slides.
DataMaper
Lets make the UserMapper responsible for separating the in-memory objects (containing only data) from the database.
class UserMapper
{
protected $gateway;
protected $hydrator;
public function __construct(TableGateway $gateway, HydratorInterface $hydrator)
{
$this->gateway = $gateway;
$this->hydrator = $hydrator;
}
public function findOneById($id)
{
$rowset = $this->_gateway->select(array('id' => $id));
$row = $rowset->current();
if(!$row) {
throw new \DomainException("Could not find user with id of $id in the database.");
}
$user = new User;
$this->hydrator->hydrate($row, $user);
return $user;
}
public function findManyBy(array $criteria)
{
// $criteria would be array('colum_name' => 'value')
}
public function save(User $user)
{
$data = $this->hydrator->extract($user);
// ... and save it using the $gateway.
}
}
For more information about the responsibility of data mappers check out Martin Fowler's definition.
Buniness Logic
It's recommended not to place any model related business logic directly into the Controller. Therefor lets just create a simple UserService which will handle validation. If your fond of form objects you could also use Zend\Form\Form in this process.
class UserService
{
protected $inputFilter;
protected $hydrator;
public function __construct(InputFilter $inputFilter, HydratorInterface $hydrator)
{
$this->inputFilter = $inputFilter;
$this->hydrator = $hydrator;
}
protected function validate(array $data)
{
// Use the input filter to validate the data;
}
public function createUser(array $data)
{
$validData = $this->validate($data);
$user = new User;
$this->hydrator->hydrate($validData, $user);
return $user;
}
}
Data Object
Now lets make the objects containing the data Plain Old PHP Objects, not bound by any restriction. This means they are not coupled with any logic and we could use them anywhere. For instance if we decide to replace our ORM with another like Doctrine.
class User
{
protected $name;
public function setName($name)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
More information about the concept of Plain Old PHP Objects can be found on Wikipedia's explanation of POJO.
If we have a code like this:
class Game {
private $_id;
private $_name;
private $_url;
public function __construct($_id,$_name,$_url){
$this->_id = $_id;
$this->_name = $_name;
$this->_url = $_url;
}
}
And we want to simply connect to our Database to get a game by id, where do we place the 'getByID' function?
Do we place it within the 'Game Class' as 'static function', do we put it in the 'Database Connection Class' as 'public function' or do we just put the method in the 'general functions inside the main index.php' as 'function'?
I currenctly have choosen for a 'static function' within the 'Game Class':
public static function getByID($id,$db){
$query = "SELECT * FROM game WHERE id = :id LIMIT 1";
$prepare = array(":id"=>$id);
$result = $db->Precute($query,$prepare);
foreach($result as $r) return new Game($r['id'],$r['name'],$r['url']);
return null;
}
(Precute is a custom function within the Database Class to prepare and execute the query)
How would you approach this?
In proper OOP, a DAL function which returns an instance of a specific class should be static within that class. As a base rule, all functionality related to one specific object should be part of that specific object, as an instance method if invoked on instances or a static method if it creates or manages instances ('factory pattern').
Your function isn't static currently, correct usage would be:
class Game
{
..other functions..
public static function getById($id)
{
..implementation, which can either access central storage or retrieve
the object itself if concurrent edits are not an issue..
}
}
Then elsewhere:
$myGame = Game::getById(684);
You may want to have a look at Doctrine instead of re-inventing the wheel. And even if you do want to make a new wheel, its code samples all follow correct OOP principles.
This Answer takes another approach. Instead of getting Objects from Static Factory. This solution takes a approach of creating a blank object and then calling the database methods to make the object a live representation of a actual row.
first the observations from your question -
an Object/Instance of Game class represents a Row of Table game. And the Game class itself can be taken as a representation of `game' table.
If the above observation is correct along with the assumption that there are more tables with a representation in class hierarchy. You should have a class to represent generic 'Table'
class Table { //The class itself can be made abstract depending upon the exact implementation
protected $_tableName;
protected $_connectionParams;
protected $idAttribute = 'id';
public function __construct($tableName, $connectionParams, $idAttribute){
$this->_connectionParams = $connectionParams;
$this->_tableName = $tableName;
if(isset($idAttribute)) {
$this->idAttribute = $idAttribute;
}
};
private function _getConnection() {
//return $db using $_connectionParams
};
public function getByID($id) {
$this->getByKeyVal($this->idAttribute, $id);
};
public function getByKeyVal($key, $val) {
$query = "SELECT * FROM ". $this->_tableName ." WHERE `". $key ."` = :key LIMIT 1";
$prepare = array(":key"=> $val);
$result = $this->_getConnection()->Precute($query,$prepare);
$this->processRow($result[0]);
};
//This needs to be overridden
public function processRow($row) {
return true;
};
}
Now extend the generic Table class for Game Table
class Game extends Table {
private $_id;
private $_name;
private $_url;
public function __construct($defaults) {
if(isset($defaults) {
if(is_array($defaults)) {
$this->processRow($defaults);
} else {
$this->getByID($defaults);
}
} else {
//Some default setup here if requried
}
$connectionParams = []; //Prepare Connection Params here
parent::__construct('game', $connectionParams);
};
//Override processRow
public function processRow($row) {
if(isset($row['id']) {
$this->_id = $row['id'];
}
$this->_name = $row['name'];
$this->_url = $row['url'];
};
}
Above is a very rough example. The actual Class structure will depend upon your requirements. But the general rule of thumb is to treat a Class as a blueprint of a concrete object. And all the methods related with a Generic Classification should go in there own class.
The getConnection Method itself can be put into a seprate DB connection class and inserted in table via a either mixin pattern or generic class inheritance.
Use the above setup like this
$game_new = new Game(); // for blank object --- for a new row
$game_435 = new Game(435); //row with 435 ID
$game_default = new Game(array( //new row with defaults
'name' => 'Some Name',
'url' => 'Some Url'
));
What you want is a "bucket" full of Game objects. When ever you want a Game Object (representing data in your database), you ask your "bucket" to give it to you. Let me give you an example of how Doctrine2 implements this:
http://docs.doctrine-project.org/en/2.0.x/reference/working-with-objects.html
So where you want to place your "getById" (or as I would do "findById"), is in your "bucket".
// lets presume that the em is an instance of \Doctrine\ORM\EntityManager
// The entity manager does what the name says.
$id = 1234;
$game = $entity_manager->find('MyNamespace\Entity\Game', $id);
$game->setName('My first game!');
// We now tell the em to prepare the object for pushing it back to the "bucket" or database
$entity_manager->persist($game);
// Now we tell the em to actually save stuff
$entity_manager->flush();
This should give you an indication of how to use it. Objects follow the Single Responsibility Principle. You don't ask an object to retrieve itself. You ask the "bucket" to retrieve you an Object.
http://en.wikipedia.org/wiki/Single_responsibility_principle
What if I told you that there are more beautiful ways to put things on their places.
A very simple case might contain 3 basic components to work:
Db framework - Which handles data access.
Table repsotor classes - Which know how to map classes to tables,
how to create classes from table data and how to create data from table classes.
Model or business layer which contain actual classes.
For better understanding imagine you have database object mapper framework.
The framework can be far complex but in few lines we can demonstrate how it`s basic
concepts work.
So the 'Framework':
<?php
//This class is for making link for db framework
class link
{
public $link;
public function __construct ($hostname, $database, $gamename, $password)
{
$this->link = new \PDO ('mysql:host='.$hostname.';dbname='.$database, $gamename, $password);
$this->link->query('use '.$database);
}
public function fetch ($query)
{
$result = $this->link->query($query)->fetch();
}
public function query ($query)
{
return $this->link->query($query);
}
public function error ()
{
return $this->link->errorInfo();
}
}
//This class collects table repositories and connections
class database
{
public $link;
public $tables = array ();
public function __construct ($link)
{
$this->link = $link;
table::$database = $this;
}
}
//This is basic table repositor class
class table
{
public static $database;
}
?>
Now as we have our db framework let us make some table repositor which knows
how to save/load/delete game:
class games extends table
{
public function create ($row)
{
$return = new game ();
$return->id = $row[0];
$return->name = $row[1];
var_export($row);
return $return;
}
public function load ($id=null)
{
if ($id==null)
{
$result = self::$database->link->fetch("select * from games");
if ($result)
{
$return = array();
foreach ($result as $row)
{
$return[$row[0]] = $this->create($row);
}
return $return;
}
}
else
{
$result = self::$database->link->fetch("select * from games where id='".$id."'");
if ($result)
{
return $this->create(reset($result));
}
else
{
echo ("no result");
}
}
}
public function save ($game)
{
if (is_array($save))
{
foreach ($save as $item) $this->save ($item);
}
if ($game->id==null)
{
return self::$database->link->query("insert into games set
name='".$game->name."'");
}
else
{
return self::$database->link->query("update games set name='".$game->name."'
where id='".$game->id."'");
}
}
public function delete ($game)
{
self::$database->link->query ("delete from games where id='".$game->id."'");
}
}
Now we can make our model which in this case will contain actuall game class.
class game
{
public $id;
public $name;
public function __construct ($name=null)
{
$this->name = $name;
}
}
And than actually use it:
$database = new database (new link('127.0.0.1', 'system_db', 'root', '1234'));
$database->tables['games'] = new games();
if (!$database->tables['games']->save (new game('Admin')))
{
var_export($database->link->error());
}
var_export($database->tables['games']->load(2));
For the moment I prefere this pattern for working with db in my projects. Using it I can achieve
that my actuall business objects(In this case class game) will know nothing about
where and how they are saved. This gives me an ability to be indipendent from
actuall storage and focus on project logics.
Also there is one lightweight framework so called db.php (http://dbphp.net) and it even
gives me ability to avoid to write table repositories and even creates/modifies tables
needed for my business classes on the fly but uses almost same concept I described here.
I'm trying to make my models ignorant of sql codes and the connection, so I am trying to implement a so called "data access object" / "dao".
My models represent tables in the database, and its properties to db fields like this for example a loginModel
class LoginModel {
private $user;
private $pass;
private $salt;
private $ip;
private $agent;
// getters and setters e.g.
public function getUser() {
return $this->user;
}
public function setUser($user) {
$this->user = $user;
}
// or for example magic methods -> just for example
public function __get($key) {
return $this->$key;
}
public function __set($key, $value) {
$this->$key = $value;
}
}
In my development, which I think I am doing the wrong thing, in my view or in my controller, I am doing this:
$loginModel = new LoginModel();
$loginModel->setUser('$_POST["user"]');
$loginDAO = new LoginDAO($loginModel);
echo $loginDAO->getResults();
Is is the proper way to do this? Or should I read the process the POST variables first in the DAO and then set the properties of the model?
If you wanted to further abstract that into different scenarios involving multiple types of storage, you may want to check out the Factory Pattern.
edit: looked at your question again, realized you didnt need half that answer but here's what i removed:
The way you design your models could be making things a little confusing. Try designing your models as objects instead of processes.
The process of a login would be handled by a controller, whereas a model would be responsible for getting things from a database. This would mean you use your models inside the controllers as a way to complete a task.
I have Yii application and two tables with same structure tbl and tbl_history:
Now want to create model so it will select table by parameter I send when calling model. For example:
MyModel::model('tbl')->find();
//and
MyModel::model('tbl_history')->find();
Find related article with solution in Yii forum. Made same changes and finally got this in MyModel:
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
Now when I make:
echo MyModel::model('tbl_history')->tableName(); // Output: tbl_history
It returns right value, but:
MyModel::model('tbl_history')->find();
still returns value for tbl.
Added:
public function __construct($id=null,$scenario=null){
var_dump($id);
echo '<br/>';
parent::__construct($scenario);
}
and got:
string(tbl_history)
string(tbl_history)
NULL
It means Yii makes call to model from other place but don't know from where and how to prevent it.
Also It makes 2 calls to model, is it too bad for performance?
It looks like the CActiveRecord::getMetaData() method needs to be overridden to achieve what you are looking for.
<?php
class TestActiveRecord extends CActiveRecord
{
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public function __construct($scenario='insert', $tableName = null)
{
if($this->tableName === 'tbl' && $tableName !== null)
$this->tableName = $tableName;
parent::__construct($scenario);
}
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
public function tableName()
{
return $this->tableName;
}
/**
* Returns the meta-data for this AR
* #return CActiveRecordMetaData the meta for this AR class.
*/
public function getMetaData()
{
if($this->_md!==null)
return $this->_md;
else
return $this->_md=static::model($this->tableName())->_md;
}
public function refreshMetaData()
{
$finder=static::model($this->tableName());
$finder->_md=new CActiveRecordMetaData($finder);
if($this!==$finder)
$this->_md=$finder->_md;
}
}
Maybe it's easier to make MyModelHistory which extends MyModel and overrides only one method - tableName().
I recommend implementing single table inheritance. In order to do this you will need to combine your tables with a flag or type column that states whether or not this is a history record. I've pasted a few links at the bottom so you can see how this is implemented in Yii and listed some of the benefits below.
Benefits:
You won't need to duplicate code commonly used between the models
Changes to this table will only need to be executed once.
Changes to the parent model will only need to be made once.
Code becomes generally more maintainable and readable.
You seperate the code that belongs specifically to tbl and tbl_history
http://www.yiiframework.com/wiki/198/single-table-inheritance/
http://en.wikipedia.org/wiki/Single_Table_Inheritance
I created a solution for performing this exact action a couple of months ago. This is a completely dynamic solution, you just pass the table name like you are looking for to the model. That solution was originally designed to work with the same database structure across multiple databases, but it was trivial to adapt it to work in the same database. The documentation for that is here. I'd recommend reading over it as it has more details about CDynamicRecord
It's easy to adapt to work with multiple tables. You can download the adaptation as a gist from github.
Usage
1) Download the Gist and drop it into ext, save as CDynamicRecordSDB.php
2) Create Your model in Model, and setup up as follows:
Basically, you want to extend CDynamicRecord, and override your model() and tableName() so they are compliant with CDyanmicRecord.
<?php
Yii::import('ext.CDynamicRecordSDB');
class Test extends CDynamicRecordSDB
{
public static function model($dbConnectionString = 0, $className=__CLASS__)
{
return parent::model($dbConnectionString, $className);
}
public function tableName()
{
return $this->dbConnectionString;
}
[... Do everything else after this ...]
}
3) Setup your model as you normally would.
Usage
The usage is identical to CActiveRecord, and you can perform all actions. No surprises. Just a couple examples below.
$data = Test::model('tbl')->findAll();
$data2 = new Test('tbl');
$data2->findAll();
foreach ($data as $row)
print_r($row->attributes);
$data = Test::model('tbl_history')->findAll();
foreach ($data as $row)
print_r($row->attributes);
Limitations
The only limitation with doing this is you have to modify how relations work. IF you plan on accessing a related model (Bar), and you have no intention on calling Bar by itself. Then Bar should extend CActiveRecord, and in Foo you can define normal relations. Yii magically carries over the CDbConnectionString across the instances for you.
OTHERWISE, if you intend to access models in the same database, but also want to retain the ability to call them by themselves, then Bar should extend CDynamicModel, and Foo should have a getter defined as follows.
public function getBar()
{
return Bar::model($this->$dbConnectionString);
}
A small way but work for me for any number of table
public static $dynamic_table_name="main_table";
public static function setDynamicTable($param)
{
self::$dynamic_table_name=self::$dynamic_table_name.$param;
}
/**
* #return string the associated database table name
*/
public function tableName($param='')
{
self::setDynamicTable($param);
return self::$dynamic_table_name;
}
// to use it like
ModelName::model()->tableName('_one');
ModelName::model()->tableName('_two');
ModelName::model()->tableName('_three');