An "abstract" DBObject class

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

Post Reply
User avatar
lorenzo-s
Forum Commoner
Posts: 43
Joined: Tue Aug 25, 2009 12:25 pm

An "abstract" DBObject class

Post by lorenzo-s »

Hi everyone.

I'm developing my own little PHP Framework. I want to create a generic DBObject class and then extend it to create, for example, User and News class. Now all my classes have something like:

Code: Select all

class User {

    public $id = 0;
    public $name = null;

    private function __construct() { }

    public static function create() {
        return new User();
    }

    public static function get($id) {
        $q = "SELECT * FROM users WHERE id = $id";
        return mysql_fetch_object($q, 'User');
    }

    public static function getAll() {
        $q = "SELECT * FROM users";
        return mysql_fetch_object($q, 'User');
    }

}
All classes have this structure. So, why not create a parent "abstract" class? With a $tableName property, for example. What stop me are all these static functions. I can't use static functions in the parent class because then I can't use object properties ($tableName) inside... So I'm searching for some tricks... I want to write classes like:

Code: Select all

class User extends DBObject {

    public $name = null;
    
    private function __construct() {
        parent::__construct('users', 'id'); // DB table and ID column
    }
    
}
And do:

Code: Select all

$u = User::get(23);
echo $u->name;
$users = User::getAll();
$new_u = User::create();
Suggestions? Thanks, Lorenzo. :D
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: An "abstract" DBObject class

Post by Christopher »

Looks good. You might want to look into the Table Data Gateway pattern.
(#10850)
User avatar
PHPHorizons
Forum Contributor
Posts: 175
Joined: Mon Sep 14, 2009 11:38 pm

Re: An "abstract" DBObject class

Post by PHPHorizons »

I don't understand why you have a get method that returns a raw array of user data. Why not instantiate the User class and if a userid is passed into the constructor, attempt to load that user's data then.

Later on, when you call get, you would pass in a field name and get that one field of user data. You can also use __get so that $this->name is mapped to $this->xxxxx['name'] which is how I'd do it.

For instance, you could do it like this:

Code: Select all

abstract class DBObject {
	
	protected $tableName;
	protected $data = array();
	
    private function __construct($config = null) {
    	
    	if (is_numeric($config)) {
    		// load some data from the db
    	} else if (is_array($config)) {
    		// manually set all data using passed in array
    	} else {
    		// simply create an empty object, with all fields empty
    	}
    	
    }

    public static function Create($config = null) {
    	$obj =  new self($config);
    	$obj->save();
        return $obj;
    }

    public static function Load($config = null) {
        return new self($config);
    }
    
    abstract public function save();
    
    public function __get($key) {
    	if (!isset($this->data)) {
    		return null;
    	} else {
    		return $this->data[$key];
    	}
    }

}

class User extends DBObject {

	protected $tableName = 'users';
	
	public function save() {
		// maybe do some validation, and then save data to the db.
	}
   
}

$u  = User::Load(23);
echo $u->name;

The GetAll function IMHO should be part of a UserCollection object and you can use the same sort of paradigm for that. One user object should operate a lot differently than an object that holds a collection of User objects since the UserCollection is only concerned with loading multiple users, and managing the array of User objects that it has collected. It might even do some sorting on that data. But any individual User operation should be in the User class. Any operation involving multiple users should be in the UserCollection class.

Cheers and hope that helps.


Edit: added missing abstract label to the DBObject class.
User avatar
lorenzo-s
Forum Commoner
Posts: 43
Joined: Tue Aug 25, 2009 12:25 pm

Re: An "abstract" DBObject class

Post by lorenzo-s »

PHPHorizons wrote:I don't understand why you have a get method that returns a raw array of user data. Why not instantiate the User class and if a userid is passed into the constructor, attempt to load that user's data then. Later on, when you call get, you would pass in a field name and get that one field of user data. You can also use __get so that $this->name is mapped to $this->xxxxx['name'] which is how I'd do it.
I do like this just because declaring all object properties will enable the IDE autocomplete feature! :mrgreen:

However your code is really interesting. Putting the DB "get one" query into the abstract class constructor in fact solve my "static functions" problem. I will try something in that way. Thank you. :)
PHPHorizons wrote:The GetAll function IMHO should be part of a UserCollection object and you can use the same sort of paradigm for that.
Yes, yes, you are right, I know. :D
User avatar
lorenzo-s
Forum Commoner
Posts: 43
Joined: Tue Aug 25, 2009 12:25 pm

Re: An "abstract" DBObject class

Post by lorenzo-s »

No, the solution cannot work. You cannot excecute $obj = new self($config); in the Create method of DBObject because that class is abstract (you cannot instantiate an abstract class). And removing abstract keyword does not work too: when you call for example User::create(23);, a DBObject is returned, not a User.

The problem seems to be that parent class cannot know who is the child class that calls its methods. In PHP 5.3 is possible with the function get_called_class(), but the servers I am using right now have PHP 5.2 installed...
User avatar
lorenzo-s
Forum Commoner
Posts: 43
Joined: Tue Aug 25, 2009 12:25 pm

Re: An "abstract" DBObject class

Post by lorenzo-s »

Waiting for PHP 5.3 support, I made it with an "emulated" get_called_class() function (taken from php.net)...
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Re: An "abstract" DBObject class

Post by Christopher »

What PHPHorizons mentioned is Active Record. True, you cannot implement static finders before 5.2, but you can implement it with an instance.
(#10850)
User avatar
PHPHorizons
Forum Contributor
Posts: 175
Joined: Mon Sep 14, 2009 11:38 pm

Re: An "abstract" DBObject class

Post by PHPHorizons »

My apologies about the Load method returning a DBObject. I normally use $user = new User($config); because calling User::Load($config) is absolutely the same end result (assuming User defined it's own Load method).

Normally I only define a static Load method when an object has an "interesting" way of being loaded. For instance, an object may not be loaded by it's primary key, but may be loaded by matching against a Unique index (like password + username). In that case, I'd make a LoadByPassword($username, $password) method which would then run a sql query to load all data for a user with that username and password and then pass that as an array to new self($config);

I hope that makes sense.

Cheers
whiteulver
Forum Newbie
Posts: 3
Joined: Fri Jul 17, 2009 7:36 pm

Re: An "abstract" DBObject class

Post by whiteulver »

I am working on a data mapper project too and my first thought was an Active Record like implementation.
But fetching 200 data records and build an instance for each of these records is really slow. Besides why i have to make an instance object when i only want is to print the fields of the record to a view?
It is usefull to make an instance of object in CRUD proccess like Active Record Pattern does. But when reading data, i think it is better to use stdClass objects with pair of key/values.
For example.
We have User model which represent a table in our database:

Code: Select all

class User extends Finder { }
we have a Finder class so we can call our find methods:
I use get_called_class() on PHP5.2 from here http://www.php.net/manual/en/function.g ... .php#93799

Code: Select all

class Finder {
  public static function find(){
    $class = get_called_class();
    $query = new Query($class);
    return $query;
  }
}
we have a Query so we can build queries:

Code: Select all

class Query extends Mysql {
  public function select($args){}
  public function where ($conditions){}
  /*
   * more function to create queries
   */
  public function fetch_one(){
    //Creates a collection object from data fetched from database and return
  }
  public function fetch_all(){
    //Creates a collection object from data fetched from database and return
  }
}
Mysql class is an adapter for mysql to execute queries build by Query class.

last we make a Collection class to iterate through results

Code: Select all

class Collection implements Iterator, Countable, ArrayAccess {

  protected $results = array();

  public function __construct($results ) {
    $this->results = $results;
  }
}
So we this is how it works:

Code: Select all

$users = User::find()->where('country_id = 10')->fetch_all();
Now $users is a collection object with data from all users from country with id = 10
We can use $users to iterate through results, use foreach() etc.

The key point is to create a very good Query class so you can build queries with associations etc.

Code: Select all

$users = User::find()->includes("country")->where('country_id = 10')->fetch_all();
The above will make a query for $user with country_id = 10, and a query from country table("SELECT * FROM countries WHERE id = 10")
Then will make a nested object like this ( well it will be more complex cause we have Collection call objects not stdClass object but i make it like this for better understanding)

Code: Select all

    [User] => stdClass Object
        (
            [1] => Array
                (
                    [id] => 1
                    [country_id] => 10
                    [country] => stdClass Object
                        (
                             [id] => 10
                             [name] => "Some Country Name"
                        )
                )

            [2] => Array
                (
                    [id] => 2
                    [country_id] => 10
                    [country] => stdClass Object
                        (
                             [id] => 10
                             [name] => "Some Country Name"
                        )
                )

        )
and we can call $user[0]->country->name to get country name.

What do you think?
Post Reply