OOP - object design granularity

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
Heavy
Forum Contributor
Posts: 478
Joined: Sun Sep 22, 2002 7:36 am
Location: Viksjöfors, Hälsingland, Sweden
Contact:

OOP - object design granularity

Post by Heavy »

This post is created as a reply to an off topic discussion started here.
jason wrote:Anyways, what you would be better off doing is separating your validation. Remove it from the user object. In fact, the user object could be thinned to something like:

Code: Select all

function login ($LoginValues)
{
        $Validations =& Inspector::validate($LoginValues);
        $Validations->enforce('username', 'password');
        if ( $Validations->isValid() ) {
                $this->setUsername($LoginValues['username']);
                $this->intLoggedInId = $this->getUserId();
        }
}
Beeing an OOP beginner, and even a more beginner of a class designer, I don't quite follow on that one. Maybe you would like to post some more of the Inspector class than just how you use it?

I guess that function login ($LoginValues) is a function inside the class user, but I dont see the whole picture.
May I ask you to provide a link to a page that describes the theory?
jason wrote:Anyways, the idea is to try and separate your validation from your User object's login method.
The user object I provided was an embryo. I get your opinion that it maybe should not even get ANY larger. But I guess I'm just a kid in object design, and would like to read more about it. I'll search the "phppatterns.com concerning Strategy patterns" and see what I get.

Now I've read it.
I see the point. It looks very clean when using it.

I'm trying to look at my user class and see what you see.
It's true that I don't validate the $strUsername and $strPassword inside the user object, but they were made safe at initialisation of $objNavigator (read below). They were not validated (ie, checked for bad format), but safe for SQL.

I realise it'll take a couple of years to reach this level of thinking. I started doing some OOP as late as the beginning of this year, but haven't tried to create something with it until summer.


Today I have larger objects than you, but I think they are pretty small and modularised anyway.

**************
I have an object called "objNavigator" that is responsible for ways to redirect the browser in different ways and to secure input. Its constructor performs a check to make sure all $_GET and $_POST values are (kind of) friendly. That's it. I chose it to be responsible for those two things.

The user object:
At least until you showed up, I thought it was good practise to let the user object contain methods to authenticate login and check privileges and such. That is/was my heading.

Why do I make a new thread out of this?
Well, as the header implies, I am interrested in a discussion on how large a class should be. I realise I might start something endless.
NoReason
Forum Commoner
Posts: 51
Joined: Tue Sep 10, 2002 6:19 pm

Post by NoReason »

Well .. My classes are pretty basic;

class object
{
// all methods and varialbes that belong to all objects
//derived from object class.
}

class user extends object
{
// all methods and variables that are used on the userObject
//which include 'post' authentication actions.,
}

I have a seperate function (not class method) that does my authentication before i instantiate my class and populate it with defualt info.
ie: username, sessionid, logintime, etc... The constructor does that all for me.
the userObject also stores any transaction information that may alter the userObject in some way .. logout (which just destroys the session), etc..

My userObject is about 40 odd lines of code that handle all of what I /currently/ need.

I have a seperate database connection and query object that handles all of db related needs.. its about 30 odd lines.
jason
Site Admin
Posts: 1767
Joined: Thu Apr 18, 2002 3:14 pm
Location: Montreal, CA
Contact:

Post by jason »

Well, as the header implies, I am interrested in a discussion on how large a class should be. I realise I might start something endless.
As large as they need to be, and as small as they can be. That's probably the best answer you will get.

More specifically, the reason you bring this up was because of my post in response to your User class.

I just want to make sure you understand that one of the best reason's I can give you for wanting to seperate that User classes login validation, and that actual login itself is that of flexibility. You see, by giving the User class a lot of control, you are making that class far more powerful than it needs to be. When you do $User->login(), you shouldn't have to care whether or not the user successfully logged in or not. All you care about is that the $User object is going to log the person in if he should be logged in, or not log them in if they shouldn't be logged in.

Inside your user's class, you places the logic to determine if the user was indeed a valid user. You did this by validating the variables that were passed to you.

However, here is where the real reason comes into play.

Your user class assumes that the login information is stored in a database. But what if suddenly, you want to also log a user in from some place else. Even better yet, you want to let the user log in based on certain information that he sends along.

Suddenly, it becomes a problem. You see, in order to change your User class to handle this, you actually have to change the User class. However, if you delegate the responsibility out to another object, in my example, an Inspector object, it becomes much easier.

Now, rather than having to worry about where you are validiting in the User class, you just know one of two things. Either this information is valid, or it's not. It's really that simple.

If suddenly you want to validate a user against, let's say, his POP3 account, or maybe his PHPBB user account, rather than go around editing your User class, you simple create new classes to handle the validation for you.

Indirection is the key. Each class only handles, or knows, what it neesd to know.

This is the reason you don't do something like this:

Code: Select all

function login ()
{
    $username = $_POST['username'];
    $password = $_POST['password'];
    // validation
}
That code limits your class from only working with $_POST variables. One day, you will want to work from something else, and you will have to make a lot of changes.

The same things for database queries. And regular expressions, and other forms of validation.

I will post a better example of the Inspector and Validation classes, and there uses, if you want.
User avatar
Heavy
Forum Contributor
Posts: 478
Joined: Sun Sep 22, 2002 7:36 am
Location: Viksjöfors, Hälsingland, Sweden
Contact:

Post by Heavy »

jason wrote: This is the reason you don't do something like this:

Code: Select all

function login ()
{
    $username = $_POST['username'];
    $password = $_POST['password'];
    // validation
}
That code limits your class from only working with $_POST variables. One day, you will want to work from something else, and you will have to make a lot of changes.
That makes your point perfectly clear :D
I get it! Thank you very much!

Any additional opinions from anyone?
User avatar
Heavy
Forum Contributor
Posts: 478
Joined: Sun Sep 22, 2002 7:36 am
Location: Viksjöfors, Hälsingland, Sweden
Contact:

Post by Heavy »

I have browsed the manual and searched the web without learning anyting about what you achieve by doing this on the first line:

Code: Select all

<?php
    $Validations =& Inspector::validate($LoginValues);
    $Validations->enforce('username', 'password'); 
?>
It seems like $Validations becomes an object with reference to the returned value of the class function Inspector::validate().

I don't really understand how this is done inside Inspector.

What I know about "::":
classname::classFunctionName makes a function call to classFunctionName as if it was a function and not a member of the class. This makes it possible to access functions within any class or a class' parent's class.

According to the comment from the manual on oop and "::" provided below, usage of the "$this" variable inside classFunctionName when being called with "::" refers to $this of the calling object. Maybe strange... I don't know. Maybe a bug :roll:

That is all I know about the :: operator.

Jason, may I ask you if you would like to post your Inspector class here, so I can learn from it?
PHP Manual Comment wrote: wikiz at studentas dot lt
15-Jun-2003 11:40
when using "::" operator inside class functions, you can achieve quite interesting results. let's take this example:

Code: Select all

<?php
  class cCat {                     
    function Miew(){
      // cCat does not have a member "kind", but cDog has, and we'll use it
      echo "I am ".$this->kind.", and I say MIEW\n";
     
      // here things are even stranger: does cCat class
      // support WhoAmI function? guess again...
      $this->WhoAmI();
    }
  }
 
  class cDog {
    var $kind = "DOG";
    function Bark(){     
       // let's make this dog act like a cat:)
       cCat::Miew();
    }                 
   
    function WhoAmI(){
      echo "Yes, I'm really ".$this->kind."!";
    }
  }

  $dog = new cDog();
  echo $dog->Bark();
?>
outputs:
I am DOG, and I say MIEW
Yes, I'm really DOG!

The interesting thing here is that cDog is not descendant of cCat nor vice versa, but cCat was able to use cDog member variable and function. When calling cCat::Miew() function, your $this variable is passed to that function, remaining cDog instance!

It looks like PHP doesn't check if some class is an ancestor of the class, calling function via '::'.
User avatar
Heavy
Forum Contributor
Posts: 478
Joined: Sun Sep 22, 2002 7:36 am
Location: Viksjöfors, Hälsingland, Sweden
Contact:

Post by Heavy »

Could it be that you do something like:

Code: Select all

<?php
class validatorParent{
	var $boolIsValid = 0;

	function & validate($arrValidationVars){
		$objVal =  new validator();
		$objVal->populate($arrValidationVars);
		return $objVal;
	}

	function enforce($strUsername = '', $strPassword = ''){
		//action taken
	}

	function isValid(){
		return $boolIsValid;
	}
}

class Inspector extends validatorParent{
	function enforce($strUsername, $strPassword){
		$this->boolIsValid = ($strUsername == 'correct' && $strPassword == 'correct');
	}
}

?>
:?: I think it feels like you do that.
jason
Site Admin
Posts: 1767
Joined: Thu Apr 18, 2002 3:14 pm
Location: Montreal, CA
Contact:

Post by jason »

Validate.php

Code: Select all

<?php

if (!defined('ECLIPSE_ROOT'))
{
	define('ECLIPSE_ROOT', '');
}

if (!defined('ECLIPSE_VALIDATE')) 
{
	define('ECLIPSE_VALIDATE', '');
	
	if ( !defined("ECLIPSE_VALIDATE_CONFIG") ) {
		define("ECLIPSE_VALIDATE_CONFIG", '');
		define('ECLIPSE_VALIDATE_VALIDATORS_DIR', ECLIPSE_ROOT.'validators/');
	}
	
	class ValidationInspector
	{
		var $validate_array;
		var $Validators;
		var $errors;
		var $_is_valid;
		
		function ValidationInspector ( $validate_array )
		{
			$this->validate_array = $validate_array;
			$this->inspectValidators();
			$this->_is_valid = true;
		}
		
		function inspectValidators ()
		{
			foreach ( $this->validate_array as $name => $value ) {
				$class_name = 'validate_'.$name;
				$file_name = ECLIPSE_VALIDATE_VALIDATORS_DIR.$class_name.'.php';
				
				if ( !file_exists($file_name) ) {
					continue;
				}
			
				include_once $file_name;
				$this->Validators[$name] =& new $class_name($value, $this->validate_array);
				
				if ( !$this->Validators[$name]->isValid() ) {
					$this->_is_valid = false;
				}
			}
			
			$this->getErrorMessages();
		}
		
		function requires ()
		{
			$args = func_get_args();
			foreach ( $args as $name ) {
				if ( !isset($this->Validators[$name]) ) {
					$this->_is_valid = false;
				}
			}
		}
		
		function isValid ()
		{
			return $this->_is_valid;
		}
		
		function getErrorMessage ( $name )
		{
			if ( isset($this->errors[$name]) )
				return $this->errors[$name];	
		}
		
		function getErrorMessages ()
		{
			if ( !is_array($this->errors) && is_array($this->Validators) ) {
				foreach ( $this->Validators as $element => $validator ) {
					if ( !$validator->isValid() ) {
						while ( $err = $validator->getErrorMessage() ) {
							$this->errors[$element] .= $err.'  ';
						}
					}
				}
			}
			
			return $this->errors;
		}
	}
	
	class Validator 
	{
		/**
		* Private
		* $errorMsg stores error messages if not valid
		*/
		var $errorMsg;
		
		var $name;
		
		var $value;
		
		function Validator ( $name )
		{
			$this->name = $name;
			$this->errorMsg=array();
			$this->validate();
		}
		
		function validate()
		{
			// Superclass method does nothing
		}
		
		function setError ($msg)
		{
			$this->errorMsg[]=$msg;
		}
		
		function isValid ()
		{
			if ( count($this->errorMsg) ) {
				return false;
			} else {
				return true;
			}
		}
		
		function getName ()
		{
			return $this->name;
		}
		
		function getErrorMessage ()
		{
			return array_shift($this->errorMsg);
		}
	}
}

?>
That is the Validate.php page. Then, in the ECLIPSE_VALIDATE_VALIDATORS_DIR directory, you would put your validators. One validator per file. So if I want to validate a username, I would do:


validate_username.php

Code: Select all

<?php

class validate_username extends Validator 
{
	var $username;
	var $db;
	
	function validate_username ( $username )
	{
		$this->username = $username;
		$this->db =& staticDatabase();
		parent::Validator('username');
	}
	
	function validate ()
	{
		if (!preg_match('/^[a-zA-Z0-9_-]+$/',$this->username )) {
			$this->setError('Username contains invalid characters');
		}
		
		if (strlen($this->username) < 1 ) {
			$this->setError('Username is too short.');
		}
		
		if (strlen($this->username) >= 20 ) {
			$this->setError('Username is too long.');
		}
		
		/* If something above failed, then we don't want to connect to the database
		because we already know it wouldn't exist. */
		if ( !$this->isValid() ) {
			return;
		}
		
		// database checking, etc.
	}
}

?>
So, all I would really have to do is something like this:

Code: Select all

<?php

$ValidationInspector =& new ValidationInspector($_POST);
$ValidationInspector->requires('username', 'password');

if ( $ValidationInspector->isValid() ) {
	$AccountSession->Login(true, AUTH_MERC, 'active');
} else {
	$AccountSession->Login(false);
}

?>
Or something like that. The idea is to move the validation outside the business logic. Validation is not important to the business logic. Does it matter if the username is properly entered or not? No, it really doesn't. All the software cares about is if the information is valid or not. If it's valid, then hey, log the guy in.

You should note that this is taken from mostly working code. Mostly, because I didn't remove code that is not important. But if you look at the example, you should see how easy it is to understand what is going on, even without comments. =)
jason
Site Admin
Posts: 1767
Joined: Thu Apr 18, 2002 3:14 pm
Location: Montreal, CA
Contact:

Post by jason »

Oh, one more thing. This code still needs to be fixed in places. There are a few bad design decisions in it that I need to take care of, but even still, it works, and works rather well for me right now.
User avatar
Heavy
Forum Contributor
Posts: 478
Joined: Sun Sep 22, 2002 7:36 am
Location: Viksjöfors, Hälsingland, Sweden
Contact:

Post by Heavy »

Right now I am working on my application trying to isolate common tasks to my "classes" and application specific tasks to my "appclasses". I think this is a good idea. My "classes" are stored one class per file in my cvs repository without beeing connected to any project.

I use them by specifying an include path in my application so that they are included, but come from outside application. (Bad sentence, hope it makes sense.)

I have settled for a solution where the choice of DB could be anything.

I have a application superclass called "transactions". its children are responsible for all db actions, select, update, create, drop, whatever. No other class is involved in db action at all.

This class is extended to other classes with a similar, but db specific names, IE:

Code: Select all

<?php

// Edit: removed unnecessary code.

function loadAppClasses() {
    $args = func_get_args();
    foreach($args as $strClassName) {
        $strClassName = strtolower($strClassName);
        $strClassPath = 'appclasses/class.' . $strClassName . ".php";
        if (file_exists($strClassPath)) {
            require_once($strClassPath);
        }
    }
}

define('DB_PROVIDER', 'mysql_innodb');

class transactions{
	var $objDB;

	function & createTransObj(){
		loadAppClasses('transactions_' . constant('DB_PROVIDER'));
		switch (constant('DB_PROVIDER')){
			case 'mysql_innodb':
				$objTrans = & new transactions_mysql_innodb();
				break;	
		}
		return $objTrans;
	}

	function updateCustomerProfile($arrProfileData){
		//dead superclass prototype
	}
}

class transactions_mysql_innodb extends transactions {
	
	function transactions_mysql_innodb(){
		$this->objDB = new DB_mysql_innodb();
	}
	
	function updateCustomerProfile($arrProfileData){
		//mysql blablabla
	}
}

// I won't post the DB... classes here... too big.

?>
I could create for example an application object that uses my "transactions" in the following way:

Code: Select all

<?php
    $objTrans = transactions::CreateTransObj();
    $objTrans->updateCustomerProfile($arrProfileData);
?>
This would be applicable whatever database we are using.
if another DB is to be used, just write another transactions_xxxxx_xxx class and another DB_xxxxx_xxx class, and define

Code: Select all

<?php
define('DB_PROVIDER', 'xxxxx_xxx');
?>
instead.
Voila. Application portable between different kinds of databases. One could even write a transaction_text_file class version if one wanted to. It'd still work.

Any opinions?
davro
Forum Newbie
Posts: 8
Joined: Fri Nov 01, 2002 6:54 am

Post by davro »

Code: Select all

<?php
/**
 *  PATTERN               strategy        Mydata
 *  Validator                  superclass   Validation
 *  ValidateString           subclass      Validates a string
 *  ValidateUser            subclass      Validates a username
 *  ValidatePassword    subclass      Validates a password with confirm
 *  ValidateEmail            subclass      Validates a email address
 */
                                                                                                                                              
if (!defined('LIBRARY_ROOT'))
{
    define('LIBRARY_ROOT', '');
}
                                                                                                                                              
class Validator
{
    var $errorMsg;
                                                                                                                                              
    function Validator ()
    {
        $this->errorMsg = array();
        $this->super();
    }
    function super()
    {
        // Superclass method
    }
    function setError ($msg)
    {
        $this->errorMsg[] = $msg;
    }
    // Accessor Returns true is string valid, false if not : return boolean
    function isValid ()
    {
        if (count ($this->errorMsg) > 0)
        {
            return false;
        } else {
            return true;
        }
    }
    function getError ()
    {
        return array_pop($this->errorMsg);
    }
}

class ValidateString extends Validator
{
    var $string;
    var $min;
    var $max;
                                                                                                                                              
    function ValidateString ($string, $min, $max)
    {
        $this->string = $string;
        $this->min = $min;
        $this->max = $max;
        parent::Validator();
    }
    function super() {
        if (!preg_match('/^[a-zA-Z0-9_]+$/',$this->string))
        {
            $this->setError("<i>$this->string</i> contains invalid characters");
        }
        if (strlen($this->string) < $this->min)
        {
            $this->setError("<i>$this->string</i> is shorter than $this->min characters");
        }
        if (strlen($this->string) > $this->max)
        {
            $this->setError("<i>$this->string</i> is longer than $this->max charcters");
        }
    }
}
class ValidateUsername extends Validator
{
    var $str;
                                                                                                                                              
    function ValidateUsername ($str)
    {
        $this->str = $str;
        parent::Validator();
    }
    function super()
    {
        $min = 6;
        $max = 20;
        if (!preg_match('/^[a-zA-Z0-9_]+$/',$this->str))
        {
            $this->setError("Username <i>$this->str</i> contains invalid characters");
        }
        if (strlen($this->str) < $min)
        {
            $this->setError("Username <i>$this->str</i> is shorter than $min characters");
        }
        if (strlen($this->str) > $max)
        {
            $this->setError("Username <i>$this->str</i> is longer than $max charcters");
        }
    }
}
class ValidatePassword extends Validator
{
    var $pass;
    var $conf;
                                                                                                                                              
    function ValidatePassword ($pass,$conf)
    {
        $this->pass=$pass;
        $this->conf=$conf;
        parent::Validator();
    }
    function super()
    {
        $min = 6;
        $max = 20;
        if ($this->pass != $this->conf)
        {
            $this->setError('Passwords do not match');
        }
        if (!preg_match('/^[a-zA-Z0-9_]+$/',$this->pass ))
        {
            $this->setError("Password <i>$this->pass</i> contains invalid characters");
        }
        if (strlen($this->pass) < $min )
        {
            $this->setError("Password <i>$this->pass</i> is shorter than $min characters");
        }
        if (strlen($this->pass) > $max )
        {
            $this->setError("Password <i>$this->pass</i> is longer than $max characters");
        }
    }
}
class ValidateEmail extends Validator
{
    var $email;
                                                                                                                                              
    function ValidateEmail ($email)
    {
        $this->email = $email;
        parent::Validator();
    }
    function super()
    {
        $min = 8;
        $max = 100;
        $pattern="/^([a-zA-Z0-9])+([\.a-zA-Z0-9_-])*@([a-zA-Z0-9_-])+(\.[a-zA-Z0-9_-]+)+/";
        if(!preg_match($pattern,$this->email))
        {
            $this->setError("Invalid email address $this->email");
        }
        if (strlen($this->email) < $min)
        {
            $this->setError("Email is shorter than $min");
        }
        if (strlen($this->email) > $max)
        {
            $this->setError("Email is longer than $max");
        }
    }
}
Post Reply