Page 2 of 2

Posted: Tue Mar 07, 2006 2:20 pm
by Jenk
One thing that OO coding provides over Procedural, is the maintainability, aka re-work, aka refactoring (I hate that word... :P)

Page.php:

Code: Select all

<?php

include 'DBClass.php';

DBClass::execQuery('UPDATE `table` SET `hits` = `hits` + 1');

//some other stuff.

?>
What is special about the above code? Well, there is no RDBMS specific code there. So if you, your client, or your host decide to move on to a new RDBMS, everything you will need to change is in DBClass(.php), meaning you can leave page.php alone. In a procedural sense, you would need to make changes to all your pages that connect to the DB.

Another beauty, which is possible in procedural as well, but much easier in OO is polymorphism. The added goodness here, in OO is the simplicity of instantiating new objects. The following is a page that connects to two different RDBMS:

Page.php:

Code: Select all

<?php

include 'DBClass.php';

$db1 = new DBClass('MYSQL');
$db2 = new DBClass('ODB8');

//skipping connection as I am lazy

$sql = $db1->exportDataBase('MyDataBase');

$db2->execQuery($sql);

?>
And yet another piece of goodness from OO is the ability to now be able to take 'DBClass.php' and use it on any other project that requires DataBase connectivity. This is the biggest 'winner' for me.

Posted: Tue Mar 07, 2006 2:43 pm
by Benjamin
arborint wrote:Which takes me to the code that you posted. What you posted is called a Transaction Script -- which is a fancy name for some code that does a bunch of thing together to produce a result. Your function GetMySQLData() does three main things. It get data from a data source, it generates HTML output from that, and it does timings on the process. All the code is interdependent on each other. So the two things that spring to mind when looking at it are Separation of Concerns and Dependencies. Those three different things really should be separated. I doesn't matter whether they become functions or classes. Separating them will make them easier to test and debug -- and it will also promote code reuse (DRY) because you might find that you can use those parts elsewhere. As for dependencies, the pervailing wisdom is that the data access functions/classes should not be dependent on any other code -- so the HTML generator would know about the data accessor, but not vice versa. Finally the timing code should be moved outside where it can be removed more easily for production use.
It actually only generates html output if there is an error, otherwise it returns all the records it retrieved in an array and immediately releases the results from memory using mysql_free_result. I guess I could put the timing code in a separate function, but it will still need to be called from the GetMySQLData function at the beginning and end. If I did that I would still have to modify the function for production use, and I would have to edit the new timing function as well. Wouldn't that make it more difficult? Other than that, this group of 3 functions would work on any page. The only requirement that I didn't post was the function that returns the link_id, in the form of the $Connect variable.

Thanks for all the comments. I am going to research all the links you posted and read the examples you wrote later today.

classes in php

Posted: Wed Mar 08, 2006 10:39 am
by Michael_C
I've noticed there isn't an init() method/function in the php classes I've looked at.

Does php look for a function with the same name as the php file??

Example:
For a class named "input2db" - would a method/function called the input2db() execute when instantiating the input2db class?

Michael

Posted: Wed Mar 08, 2006 10:43 am
by feyd
In PHP 4, it looks for a function with the same name as the class. In 5, it will look for a function called __construct as well.

Posted: Wed Mar 08, 2006 12:21 pm
by Michael_C
Thanks for the info
Michael

Posted: Thu Mar 09, 2006 6:02 am
by Maugrim_The_Reaper
It seems the topic got a bit sidetracked...;)

In a nutshell, unless you heavily invest in a function library OOP and classes have a few beneficial effects...

1. You spend more time designing (whether on paper, UML or with Unit Tests) and less time coding. Despite the intuitive reponse, this is a good thing. It lets you see the bigger picture. Planning is not evil - anyone who says differently probably spends their entire working day backtracking and "refactoring" after they finish something. There are some exceptions - notably the Agile Programming approach of Test Driven Development - but then that saves time from another direction, and does involve planning at some point.

2. You create fewer bugs. As well as coding less, OOP promotes good design, specialisation (each class does one and only one thing - and does it very well), simple easy to test methods, etc. It's harder to create bugs that are difficult to find - esp. when you regularly test and peer review.

3. It makes maintenance easier. Using classes you are not (or should not be) duplicating identical blocks of code across multiple files. If you see duplication its usually a "code smell" telling you it belongs in a class where its centralised and its potential bugs not copied across the entire application in a dozen locations!

4. Classes should (in an ideal world) be reuseable. If you create a good Database class, you can use it in other applications you start building. Eventually you may even build up a toolbox of useful classes - making development faster. Its important enough to note this is usually not what's done in "PHP Application Frameworks". Most are so interdependent you can't use their classes in isolation in something else. Other framework libraries like ezComponents, Horde and recently Zend Framework achieve this a lot better.

5. Classes are simple - the "complexity" and "difficult transition" are pure myths. All you need is a walkthrough of OOP, a few examples, and practice. It looks complex at first because its not immediately intuitive - that changes quickly.

6. All the cool kids are doing it ;) Well, not all, but many.

7. OOP/Classes promote the use of proven designs - Design Patterns. They make mince meat out of common problems, and well worth the time to learn and experiment with. These are ideas - not implemented code. You implement them in your own style most f the time - they just save you the time in concocting solutions on your own.

8. I've run out of points...bleh!

An example.

I dislike lumbering third party users with a heavy API so I like to "bundle" sets of functionality - let's say database reads and writes - into tiny packages. These are based on a Design Pattern, either Data Access Object or Active Record. Both basically give the idea of assembling any database table row into a single class with a simple interface.

A user example, User is a class which does nothing but hold the data from a row on the User table. These are accessed by a few method (getters) which match table fields. The data can be read or written to the database using a few CRUD methods. These actually just delegate to methods in a singular class called Data Acess Object which all such Data classes use.

This is actually in use so it probably makes some sense reading through it.

Code: Select all

// user with id=1, get, change name. then create new user with other name.

require('TransferObject.php');
require('DataAccess.php'); // DAO class - in actual code its injected into TO class's constructor as a singleton
require('User.php'); // Data class, extends TransferObject

// no sql, no long code blocks, just a simple API 6 year olds can use...
$user = new User();
$user->getByPk(1); // fetch by primary key, usually a unique field named "user_id"
$user->setName('Maugrim');
$user->save();

// new user

$user2 = new User();
$user2->setName('agtlewis');
$user2->save(); // save either updates/creates. It's tracked inside User class whether this is an existing row, or a new row.

echo $user->getName() . ' Updated.<br>';
echo $user2->getName() . ' Created.';

I'll stick the actual code below or parts of it at least (it's not perfect, but it does work as I need it to) - its open source stuff so puzzle over this if you like... It's a mishmashed set of classes for reading and writing database rows by automatically generating the SQL and executing it (via ADOdb-Lite), based on which fields are changed (touched), whether the row already exists, etc. Row objects I call Transfer Objects (differs among developers). Maybe this'll offer some small insight into OOP over a complex operation aimed at providing a simple API to users (without them needing to know anything more) - one of the cool things about OOP esp. here where I needed a simple API for third party module writers - important for a extendable game (what I'm currently working on).


Code:

Code: Select all

class TransferObject {

	var $exists = false;
	var $data = array();
	var $touched = array();
	var $primaryKey;
	var $tableName;
	var $transferName;
	var $dao;

	function TransferObject($fields=null) {
		$appHelper =& ApplicationHelper::getInstance();
		$this->dao =& $appHelper->getDAO(); // just using an apphelper to store a single instance of
											// DAO (this is the Singleton Pattern essentially)
											// can be injected into this class in other ways.
		if($fields)
		{
			$this->import($fields);
		}
	}

	// shortcut for creating a new TO from a raw associative array returned by the database without
	// calling all the individual setters in the child class, i.e. setThis(), setThat()...
	function import($fields=null) {
		foreach($fields as $key=>$val)
		{
			$this->setValue($key, $val);
		}
	}

	function &getArray() {
		return $this->data;
	}

	function getValue($key) {
		return $this->data[$key];
	}

	function setValue($key, $val) {
		$this->data[$key] = $val;
	}

	function getPrimaryKey() {
		return $this->primaryKey;
	}

	function getPrimaryKeyValue() {
		return $this->data[$this->primaryKey];
	}

	function setPrimaryKeyValue($val) {
		$this->data[$this->primaryKey] = $val;
	}

	function getTransferName() {
		return $this->transferName;
	}

	function getTableName() {
		return $this->tableName;
	}

	function getExists() {
		return $this->exists;
	}

	function setExists($var = true) {
		$this->exists = $var;
	}

	function getTouched() {
		$this->touched = array_unique($this->touched); // remove duplicate values
		return $this->touched;
	}

	function clearTouched() {
		$this->touched = array();
	}

	function getByPk($value=null) {
		if(isset($value))
		{
			$this->dao->getByPk($this, $value);
		}
	}

	function save() {
		$this->dao->save($this);
	}

	function delete() {
		$this->dao->delete($this);
	}

	function getRow() {
		$this->dao->getRow($this);
	}

	function getAll() {
		return $this->dao->getAll($this);
	}

}

class User extends TransferObject {

	function User() {
		parent::TransferObject();
		$this->tableName = 'myapp_user';
		$this->transferName = 'User';
		$this->primaryKey = 'user_id';
	}

	function getUserId() { return $this->data['user_id']; }
	function setUserId($var) { $this->data['user_id'] = $var;  $this->touched[] = 'user_id'; }

	function getUserName() { return $this->data['user_name']; }
	function setUserName($var) { $this->data['user_name'] = $var;  $this->touched[] = 'user_name'; }

}

class DataAccess {

	var $db;
	
	function DataAccess(&$db) {
		$this->db = $db;
	}

	function &getInstance(&$db) {
		static $thisInstance;
		if(is_null($thisInstance))
		{
			$thisInstance =& new DataAccess($db);
		}
		return $thisInstance;
	}

	// SQL here is based on the ADOdb-Lite or ADOdb ability to escape/quote variables automatically
	// ? stands for locations to bind quoted/escaped variables into the statement.
	// $this->db is the ADOdb-Lite instance - Execute simply passes the query to the currently set DBMS
	// usually PostgreSQL, but MySQL works too.
	function &getByPk(&$to, $value=null) {
		$sql = 'SELECT * FROM ' . $to->getTableName() . ' WHERE ' . $to->getPrimaryKey() .' = ?';
		if(isset($value))
		{
			$result = $this->db->Execute($sql, array( $value ));
		}
		else
		{
			$result = $this->db->Execute($sql, array( $to->getPrimaryKeyValue() ));
		}
		if(!$result)
		{
			trigger_error($sql . '<br /><br />' . get_class($this) . ': ' . $this->db->ErrorMsg(), E_USER_ERROR);
		}
		if(!empty( $result->fields ))
		{
			$to->import( $result->fields );
			$to->setExists(); // TO-> this row already exists, on next save() update not create!
		}
		else
		{
			$to->data = false;
		}
		$result->Close();
	}

	function save(&$to) {
		if($to->getExists()) // if it already exists we update not create!
		{
			$touched = $this->getTouchedFields($to);
			if(empty($touched['fields']))
			{
				// Note: Because we assume no errors, a mistake in correctly capturing
				// changed fields by the relevant child TransferObject will not be reported
				// as an error - this should be part of the test suite!
				return; // we don't update rows which haven't changed!
			}
			$sql = 'UPDATE ' . $to->getTableName() . ' SET ' . implode(',', $touched['fields'] ) . ' WHERE ' . $to->getPrimaryKey() . ' = ?';
			$touched['values'][] = $to->getPrimaryKeyValue();
			$result = $this->db->Execute($sql, $touched['values']);
			if(!$result)
			{
				trigger_error($sql . '<br /><br />' . get_class($this) . ': ' . $this->db->ErrorMsg(), E_USER_ERROR);
			}
			$to->clearTouched(); // we saved, so remove prior tracked changed fields
		}
		else // if it doesn't exist we create!
		{
			$insertArray =& $to->getArray();
			//unset($insertArray[ $to->getPrimaryKey() ]); // inserted automatically by pg sequence
			$sql = 'INSERT INTO ' . $to->getTableName() . ' (' . implode(',', array_keys( $insertArray )) . ') VALUES (';
			// this is way too complex for comfort, alternative?
			$a2 = str_repeat('? ', count($insertArray));
			$a1 = explode(' ', rtrim( $a2 ) );
			$bindings = implode(',', $a1);
			$sql .= $bindings . ')';
			$result = $this->db->Execute($sql, array_values($insertArray));
			if(!$result)
			{
				trigger_error($sql . '<br /><br />' . get_class($this) . ': ' . $this->db->ErrorMsg(), E_USER_ERROR);
			}
			if($to->getPrimaryKeyValue() === NULL)
			{
				$to->setPrimaryKeyValue( $this->db->Insert_ID() );
			}
			$to->setExists(); // let the DAO (if called again) know this row now exists
			$to->clearTouched(); // we saved, so remove prior tracked changed fields
		}
	}

	// Last 2 methods are basically for splitting fieldnames from values. Since we use variable binding in statements
	// to auto escape/quote values we need fieldnames and values in 2 separate arrays for the Execute method

	function getTouchedFields(&$to) { // get tracked fields which have changed for update statements/get()'s
		$touched = array();
		$touched['fields'] = array();
		$touched['values'] = array();
		$touchedArray = $to->getTouched();
		$fields =& $to->getArray();
		foreach($fields as $key=>$val)
		{
			if(in_array($key, $touchedArray))
			{
				$touched['fields'][] = $key . ' = ?';
				$touched['values'][] = $val;
			}
		}
		return $touched;
	}

	function getSetFields(&$to) { // get all tracked fields which have values (not NULL)
		$setFields =& $to->getArray();
		$fields = array();
		$fields['fields'] = array();
		$fields['values'] = array();
		foreach($setFields as $key=>$val)
		{
			$fields['fields'][] = $key . ' = ?';
			$fields['values'][] = $val;
		}
		return $fields;
	}
}

Posted: Thu Mar 09, 2006 5:25 pm
by Benjamin
Thanks a lot Maugrim_The_Reaper. I will study that.