content management theory

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
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

content management theory

Post by thinsoldier »

-- Background --
I've got this basic content management system that has News & Events. The only difference between the 2 is that Events has a start & end date. There is a News table in the database and an Events table.

On the public side both sections are in their own single .php file that generates multiple possible pages:
search form (?cmd=search)
search results
view an item (?cmd=view)
view a list of most recent items (?cmd=start)

Any News or Calendar entry can have an infinite number of images, audio files, video files, & documents assigned to it for display on the public side.

News and Calendar entries can have any number of News Categories or Calendar Categories assigned to them. These categories can have 1 level of subcategories.

Over the last year, multiple clients have requested additional sections to their site that once again vary only slightly (if at all) from the basic News/Events format. For example, the Download Library section is NO different from the News section except that it uses "download categories" instead of "news categories" and on the public side we just don't show anything except the list of downloadable files. Same way with "galleries".

As a result there have been times where I've just used a sub-category of the News Categories to impliment the Download Library section on the public side of a site.

This resulted in lots of copy/pasting of the same code on both the admin and display sides of many sites. :(
Usually a client just wants a new section that is literally no different from an existing section so we can just throw a switch in an existing include file to accommodate the new section but sometimes they want a new section that would require removing one or 2 unused fields or adding at most 1 additional field and so we just copy/paste an existing include and rip out the unused bits, find/replace the 'name' of the 'thing' (News becomes Reviews), duplicate the news table, rename it to reviews, add a new category section for review categories, done.

And of course if I find a flaw in one file I've then got to go searching through tons of other files to fix the same bug over and over.
And when a client wants to greatly expand on an area that was initially hacked out of an include used for another area instead of being it's own copy that means a lot of writing from scratch and exporting 'download library' data from the News table to then import into the new proper custom written Download Library. :(

There's also this other thing called Departments which is kind of its own section like New/Events but at the same time different Departments can be used to categorize entries in all other sections. So a news item can be in the "Press Releases->Charity" News Category/sub-category and also in the "Philanthropy" category and also in the "River City Office" Department and also in the "Northern Region -> Public Relations" Department (multiple categories and multiple departments with possible Subs of each)

So there may be a public display of departments that follows the mold of a news article: Headline(name), descriptive text(article), photos of department-members (photo gallery), introductory video for new employees (audio/video gallery), job application forms (pdf download library) etc... and then there'll be news articles in the news section relative to that department and I'm sure somewhere in the future some client is going to want departments that are within larger departments that are within larger departments etc... and I have no clue how to even begin to think about all that.

-- Examples --

http://tinyurl.com/36fpd8 [news, events, 'diaries', 'races', 'galleries']
http://tinyurl.com/2t6zuh [news, events, 'parks']
http://tinyurl.com/2k4gdf ['downloads']
http://tinyurl.com/2jmtvv [news, events, 'issues', 'multimedia', 'galleries', 'blogs']



-- Questions __

Is it a bad idea to use a single .php file to do that functionaly for displaying on the public side?
Would I be better off with a page that just searches any section, just displays search results for any section etc?

What's a good technique for implimenting an infinite number of categories->sub-categories->SubSub-categories->etc...

Any suggestions for rebuilding my system using Objects? (I've used classes to build a pretty ok pagination class and a few other basic things but I've still never needed to use inheritance or any other advanced featured of OOP)

Do you prefer to name youd database tables in the singular or plural?

and 20 other questions I forgot while remembering all the background stuff I wanted to convey at the beginning :(
Last edited by thinsoldier on Tue Aug 07, 2007 10:48 pm, edited 1 time in total.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I don't see any problem with what you are doing other than the code duplication (and your sites look very nice). One way to solve that is to create reusable classes that provide the data for the specific sections. These are usually called the "Model." Since your underlying code is similar, you could create a base Model class that provides a common set of services. Then create specific Models for each section that inherit the base and provide configuration information such as the table and column names, plus add additional methods needed by that section. Then when you change the base class, all the children inherit the changes.

You also might want to look into using a Front Controller and MVC. Both can help combine duplicated code into centralized code.
(#10850)
User avatar
superdezign
DevNet Master
Posts: 4135
Joined: Sat Jan 20, 2007 11:06 pm

Post by superdezign »

Since you are familiar with the different versions of the News and Events applications that your clients like, you could easily create class structures to handle the different apps. And since News and Events are close to one another in form, you could probably even find a common abstract between the two and create some sort of Output() function that every class would have, and you could display them all dynamically, probably through a factory if you could. I could see that simplifying a lot. Client asks for it, and they get it in a few seconds. ;)
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

#1 - I can't take credit for the visuals of any of the sites (I just did the CSS)

Thanks for the info.

MVC? Multiple Version Control or Model View Controller?

I've written a little OOP for minor stuff but never anything complex enough to actually require using inheritance. When I think about it I usually can't quite decide what should be in the base and what should be in the offspring. I can barely decide what should be in a class and what should be a globally available regular function :(
matthijs
DevNet Master
Posts: 3360
Joined: Thu Oct 06, 2005 3:57 pm

Post by matthijs »

MVC is Model View Controller. Searching for these words on this forum (or sitepoint or example) will give you a lot of good threads. You probably should read a bit about general OOP as well. I could recommend some books if you're interested.
When I think about it I usually can't quite decide what should be in the base and what should be in the offspring. I can barely decide what should be in a class and what should be a globally available regular function
What do you think? In your first post you described exactly why you had a feeling your current way of working wasn't right. All the duplication of code which makes the code hard to maintain, etc. So, your solution: put everything you want to reuse in a central place.

What helps for me is to think about real life. You have your house and once in a while you need something fixed/done. The electrics need repair. So you call an electrician. Your neighbors also have electric that needs to be fixed. They can also call the electrician. You might need a plumber sometimes. Every day the mailman passes by and brings the post. You see how that works? There are specific craftsmen (or women) who can do one thing very well. And can be reused.

It wouldn't make any sense if each household would have someone/something in house to do everything.

Also, when calling the help for something, you don't care about how it's done, isn't it? You just want your roof fixed. It's the responsibility of the professional to do his thing. You call him in, tel him what to do, and pay.

Code: Select all

$mygardenproject = new garden();
$mygardenproject->insertTree();
echo $mygardenproject->viewGarden(); // wow, this is the stupidest example I ever made 

$mymail = new Mail();
$today = '11-10-2007';
$todaysmail = mymail->getMail($today);
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

thanks for the info.

I've been put on a different project for the moment so I'll have to come back to this stuff.

I'll try implementing a basic mvc between now and then.
User avatar
PrObLeM
Forum Contributor
Posts: 418
Joined: Sun Mar 07, 2004 2:30 pm
Location: Mesa, AZ
Contact:

Post by PrObLeM »

matthijs wrote:

Code: Select all

$mygardenproject = new garden();
$mygardenproject->insertTree();
echo $mygardenproject->viewGarden(); // wow, this is the stupidest example I ever made 

$mymail = new Mail();
$today = '11-10-2007';
$todaysmail = $mymail->getMail($today);
syntax error fixed.
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

ok, changing subject for a minute.

haven't written anything OO in many months. I had started on this thing a while ago.
I want to have an object for handling showing data on a form from the database.
Another for updating/Inserting to the database
Another for generating the html form markup.
this is what that was supposed to be:


Code: Select all

<?

/**
* Value of field '$subject' in current MySQL row.
* It's like postReturn except it's run against an array of data from the database instead of $_POST.
* @parameter string $subject The name of the key being checked
* @parameter mixed $if_false The value to return if $subject comes up false
* @return mixed Will return value of $subject on true or value of $if_false on false, whatever it may be.
*/
function dbdata($subject,$if_false='')
{
	global $dbdata; // whatever is the name of your result resource variable containgin all your database values
	if(isset($dbdata[$subject])) { 
	return stripslashes($dbdata[$subject]);   
	
	} ////echo $dbdata[$subject];
	else {return $if_false; }
}

/**
* Very often in our forms that both insert and update data with a single form we'd use a combo of postReturn() and dbdata() as the value of form inputs. That double function call in the cramped space of an input tag can get pretty long sometimes so this function replaces that with a shorter single function.
* @paramater string $fieldname The key in either $_POST or $dbdata being checked.
* @paramater string $if_false The value returned if both postReturn() and dbdata() come up false.
*/
function sourceData($fieldname, $if_false='')
{
	return postReturn($fieldname, dbdata($fieldname, $if_false));
}





/**
* FormField Class- form input generator class

* @todo Implement checkboxes, radios, etc

*/
Class FormField
{

/////////////////////////////////////////////////
// PUBLIC VARIABLES
/////////////////////////////////////////////////

public $label;

public $required = false;

public $error = false;

public $errormessage = '';

/**
* Class placed on <label> of required fields
* @var string
*/
public $required_classname = 'required';
public $error_classname = 'error';

/**
* <input name="">
* @var string
*/
public $name;


/**
* text, textarea, select, radio, etc
* @var string
*/
public $type;


/**
* text next to radio/checkbox
* @var string
*/
public $text;  


/**
* preset value in the calling script using usual postreturn & dbdata
* seems I used sourceData() everywhere instead of this var so maybe it's not needed at all?
* @var string
*/
public $value;  


/**
* <label id=""><input id=""> ID is important for label/field pairing accessibility
* @var string
*/
public $cssid;  
	

/**
* Extra stuff like javascript or classname on the field tag.
* Must be an array of 3 items if used with type selectYMD
* @var string
*/
public $extra;  

// vars for text
public $size = 64;
public $maxlength = 100;

// vars for textarea
public $rows = 15;	// for textarea
public $cols = 30;// for textarea



// vars for select

/**
* Array of possible options <option value=$key>$value</option>
* @var array
*/
public $options = array(); // 


/**
* whether select builder uses array keys as values
* 
* @var string
*/
public $select_key;


public $firstword = 'Select';

// vars for selectYMD
public $year_range=array(); // range of years

public $before_output = "\r\r<div>\r";
public $after_output ="\r</div>\r";


/**
* The combined string that holds the fields generated html
* @var string
*/
public $html = ''; // 



/////////////////////////////////////////////////
// METHODS
/////////////////////////////////////////////////

/**
* Arguements for this constructor set the class variables.
* No html is generated at construction time because depending on the type of form field you want, you may have to define additional paramaters in order for that type to work propertly and those paramaters aren't covered in the constructor.
* 
* @parameter string $name Name of input field. $prefix value if $type is selectYMD
* @parameter string $type Type of input. password, hidden, text, select, selectYMD, 
* @parameter string $label Text for <label>
* @parameter string $cssid ID value to tie the Label and Input field together
* @parameter bool $required Whether or not the field is required. Will cause a * to be placed before label name and $required_classname to be applied to <label class="">
* @parameter string $errormessage Error message to show the user on submit. Only needed if $required is TRUE.
* @todo Switch first 2 arguements so that $type comes first
*/
function __construct($name='', $type='', $label='', $cssid='', $required=false, $errormessage='')
{
	$this->name = $name;
	$this->type = $type;
	$this->label = $label;
	$this->cssid = $cssid;
	$this->required = $required;
	$this->errormessage = $errormessage;
}


/** 
* Generate hidden input 
*/
public function hidden()
{
	if($this->type == 'hidden'){
	$this->html .= '<input '.$this->extra.' type="hidden" name="'.$this->name.'" value="'.sourceData($this->name).'" />';
	}
}


/** 
* Generate text input 
*/
public function text()
{
	if($this->type == 'text'){	
		$cssid = (!empty($this->cssid)) ? ' id="'.$this->cssid.'"' : null;
		$size = (!empty($this->size)) ? ' size="'.$this->size.'"' : null;
		$maxlength = (!empty($this->maxlength)) ? ' maxlength="'.$this->maxlength.'"' : null;
		
		$this->html .= '<input'.$this->extra.' type="text"'.$cssid.' name="'.$this->name.'" value="'.sourceData($this->name).'"'.$size.$maxlength.' />';
	}
}



/** 
* Generate textarea input
* @see FormField::markdown()
*/
public function textarea()
{
	if($this->type == 'textarea'){
	$cssid = (!empty($this->cssid)) ? ' id="'.$this->cssid.'"' : null;
	$this->html .='<textarea'.$this->extra.$cssid.' name="'.$this->name
					.'" rows="'.$this->rows.'" cols="'.$this->cols.'">'
					.sourceData($this->name).'</textarea>';
	} else { };
}



/** 
* Generate textarea with Markdown toolbar WYSIWYG
* @todo Yet to be implemented
*/

public function markdown()
{

}


/**
* Generates select menu using {@link selectBuilder()}
* @todo not finished, what about sourceData?
*/
public function select()
{
	if($this->type == 'select')
	{
		// extra must contain id if cssid is set
		
		$extra = (!empty($this->cssid)) ? ' id="'.$this->cssid.'"' : null;
		$extra .= $this->extra;
		
		$key_nokey = ($this->select_key == true) ? 'key' : 'nokey';
		
		$this->html .= selectBuilder($this->name, $this->options, sourceData($this->name),$key_nokey,$extra, $this->firstword);
	}
}


/**
* Generates month-day-year select boxes using the selectBuilder() function.
* 
* @parameter string $prefix Required prefix string gets added to the name of each select
* @parameter string|array $preselect The mysql date string used for the default values
* @parameter array $extra 3 item array containing extra properties for each <select> tags
* @parameter array $range Uses range() to generate an array of years
*/
function selectYMD()
{
	if($this->type == 'selectYMD')
	{	
		$prefix = $this->name.'_';
		if(empty($this->range)){ $range = range(2020, 1990); }
		if(empty($this->extra)){ $extra = array('','',''); }

		 // css id value will always be set on the day select box
		 // so when a user clicks the label it will highlight the first select which is Day.
		$extra[2] .= ' id="'.$this->cssid.'" ';

		$yearBox 	= selectBuilder($prefix.'year', $range, sourceData($prefix.'year',date('Y')),  'nokey', $extra[0], 'Year');
		$monthBox 	= selectBuilder($prefix.'month', sb_months(), sourceData($prefix.'month',date('m')), 'key', $extra[1], 'Month');
		$dayBox 		= selectBuilder($prefix.'day', range(1,31), sourceData($prefix.'day',date('d')), 'nokey', $extra[2], 'Day');
	
		$output = "<!-- Select Year Month Day -->\n".$dayBox.$monthBox.$yearBox."\n<!-- end -->\n";
		
		$this->html = $output;
	}
}




/** 
* Every type of form field has a function name that matches its type. GetField looks at the $type and then calls that function.
* That function generates the html for the input field and then GetField gives it an associated label.
* The returned string can be wrapped with {@link $before_output} and {@link $after_output}
* 
* @return string
*/
public function GetField()
{
	$type = $this->type;
	$this->$type(); // call the function named after the type

	$for = (!empty($this->cssid)) ? 'for="'.$this->cssid.'"' : null;
	$req = ($this->required) ? $this->required_classname : null;
	
	$err = ($this->error) ? $this->error_classname : null ;
	
	$class = ($this->required || $this->error) ? "class=\"$req $err\"" : null;
	
	$star = ($this->required) ? '* ' : null; // place * in front of required field labels

	$content = "\n<label $for $class>".$star.$this->label."</label>\n".$this->html;
	
	return $this->before_output . $content . $this->after_output;
}

/** 
* Calls the GetField method automatically if you try to echo the object var.
* @return string
*/

public function __tostring()
{
	return $this->GetField();
}

}
?>
Now, when I actually went to use it for the first time:

Code: Select all

$quickfields = new FormField('formcat','select','For Sale or Rent');
$quickfields->options = array('Buying','Renting');

echo $quickfields;
I realized I should have had $options as an argument on the __construct() and then got to thinking that in order to be able to create _any_ kind of form field in just 1 line of code I'd need to have _every_ optional paramater as an argument of __construct.

My goal is to be able to define a field in as few lines as possible, mostly using just the _construct. But having this one class to try to handle all the variables of all the different kinds of inputs is always going to be a bit wordy I guess.
I think I'm probably going to need to have a separate class for each input type.
Right?
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

This is where you get to learn about inheritance. You might want to read a bit about it. A good reference is Head First Design Patterns from oreilly press. The beginning chapters cover creating classes and subclasses, but here we go.


Write down your requirements. In this case you are going to have a set of objects that will create form input fields. For each form field write down what data is needed and what it should do. For instance a select field has some options, a selected value, a style, an id, etc.


Look and see what all of your input have in common. Probably name, id, a type, maybe a value etc.


Create an interface to set the properties that all of your inputs have in common. This will be a contract that all of your fields will need to adhere to.

Code: Select all

interface FormInput_Interface
{
  public function setName($name) ;
  public function setID($id) ;
  public function setValue($value) ;
  public function __toString() ;
}

Next create an abstract object that some of your inputs will inherit from

Code: Select all

abstract class FormInput_Abstract implements FormInput_Interface
{
  protected $name ;
  protected $id ;
  protected $value ;

  public function setName($name)
  {
    $this->name = $name ;
  }

  public function setID($id) 
  { 
    $this->id = $id ;
  }

  // ... yada yada but don't implement __toString() ;

}

Now lets make some concrete classes that have some more specific stuff

Code: Select all

class FormInput_Text extends FormInput_Abstract
{
  public function __construct($name, $id, $value)
  {
     $this->setName($name) ;
     $this->setID($id) ;
     $this->setValue($value) ;
  }

  public function __toString()
  {
     return "<input type=\"text\" name="{$this->name}" id="{$this->id}" value="{$this->value}" />" ;
  }
}

class FormInput_Select extends FormInput_Abstract
{
  protected $options ;

  public function __construct($name, $id, $value, $options)
  {
     $this->setName($name) ;
     $this->setID($id) ;
     $this->setValue($value) ;
     $this->setOptions($options) ;
  }

  public function setOptions($options)
  {
     $this->options = $options ;
  }

  public function __toString()
  {
     // using the options and other stuff create and return a select thingy
  }

}

And then we can use it

Code: Select all

$select = new FormInput_Select('myselect', 'someid', '1', array('1' => 'yes', '0' => 'no') ) ;
$text = new FormInput_Text('mytextinput', 'textID', 'stuff') ;

echo $select ;
echo $text ;

I am sure others will nitpick about my specific implement here (like using getters/setters for everything), which is fine, but I think the overall idea of this is what you will want to use for your form inputs.
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

Thanks :)

1st question:
Is the interface really necessary?

I started heading down this direction (sort of) yesterday before reading your post. I've got a base class which I extend for the other classes. But what is the purpose/benefit of also having the interface before that?
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

The interface is not always necessary but good practice. You should always be programming to interfaces and not to implementations. Take a hammer and repeatedly pound that into your head until it is a mantra.

Take this example

Code: Select all

class Order
{
   public function getID() { ... }
}

class OrderProcessor()
{
  public function process( Order $order ) {...}
}


$order = new Order() ;
$processor = new OrderProcessor() ;

$processor->process($order) ;
Lets say you do the above and its all fine and dandy. Notice that process() will only take objects that are of class Order. Now lets say you code hundreds of methods that all take orders as an argument.

Now lets say you realize you need to add in another order type, say backorders and its so different that it doesn't make sense to use an abstract order class. Had you used an interface for order you could just have backorders implement that same interface and be done with it. Now you are in a bind.


The other advantages of an interface is that you can apply an interface to other objects. For instance with your FormInput class you could have this (although its probably not a good design):

Code: Select all

class Product_Category implements FormInput_Interface
{
}
Now you could have a product category class that not only represents a category but could also be used to draw a selection input which showed available categories and had one of them selected.
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

ok Begby. I refactored my form fields based on your input but I think I'm not going to use the interface stuff just yet. I'll wait until that decision actually comes back to haunt me so when I learn that lesson it'll actually stick :)


Could you give some tips on re-factoring this other thing?

see:
http://www.paradisebahamas.com/communit ... each/36/0/
http://www.paradisebahamas.com/search.p ... island=252

How I display the list of properties on both pages is I have a file that I include() searchresults.php and there are variables that I set in the file that calls the include() which affect things in searchresults.php like the number of records to display per page and the starting point sql to find the grand total which then gets split up into pages.

I'm starting to think I could just turn the included file into a class and make the variables I set in the calling file properties of that class.

for the search.php page if you submit the search form the script includes another file that I reuse on many sites which performs a bunch of if() statements based on the submitted info and generates the starting point sql which is then used with the include(searchresults.php)

I do _a lot_ of real estate related stuff so I'd really like to not have to keep copying these include files from site to site

I have a propertydisplay class already that gathers all the info needed to display a property:
description
all photos/videos of that property from the multimedia database
convert values like 2.5 into "2 1/2 bathrooms"
convert island/city/neighbourhood id's into their string values
change 345123 into $345,123.00
etc...
Begby
Forum Regular
Posts: 575
Joined: Wed Dec 13, 2006 10:28 am

Post by Begby »

For a model (in this case a property model) I have been using a mapper, domainObject, and a Validator. It looks like this

Code: Select all

// add a product
$product = new Product() ; // Domain object
$mapper = new ProductMapper() ;
$validator = new ProductValidator() ;

$product->setSKU('SKU') ;
$product->setPrice(1.85) ;

if ($validator->validate($product) )
{
 $mapper->insert($product) ;
}
else
{
  // redisplay the form and show the errors
  $this->view->errors = $validator->getErrors() ;
}



// Fetch a product and update it
$mapper = new ProductMapper() ;
$validator = new ProductValidator() ;
$product = $mapper->findByID(10) ;

$product->setPrice(1.80) ;
if ($validator->validate($product))
{
  $mapper->update($product) ;
}
Now here is what I do for getting search results or something else

Code: Select all

$mapper = new ProductMapper() ;
$products = $mapper->searchAndFetchPage('productNameToSearchFor', $pageNumber, $recordsPerPage) ;

foreach ($products as $product)
{
   echo $product->getSKU() .'<br />';
}

I can see how something similar might work for your properties class.

Keep in mind though a difference between object properties and parameters. Setting a property should affect the instance of the object and all method calls. If you have to change an object property every time before you call a method, then you probably want that to be a parameter instead like this

Code: Select all

$properties->searchCity = 'tampa bay'
$properties->search() ;

// should be

$properties->search( 'tampa bay' );

// or

$searchParams = new PropertiesSearchParams() ;
$searchParams->setCity('tampa bay') ;

$properties->search($searchParams) ;
thinsoldier
Forum Contributor
Posts: 367
Joined: Fri Jul 20, 2007 11:29 am
Contact:

Post by thinsoldier »

:( I've read this thread a dozen times and still don't quite comprehend your last 2 comments begby :(
Post Reply