Jump to content

Implementing Factiories & Reflections to Handle Content Dynamically & Modularly


c4onastick

Recommended Posts

Continuation of this thread.

 

How do you implement dynamic content using factories and reflection?

 

Let's say I have a class for displaying a page that looks something like this:

<?php
class page {
   var $content;

   function display() {
      $this->display_header();
      $this->display_body();
      $this->display_footer();
   }

   private function display_header() {
      echo "<html>\n<head><title>Practice with OO PHP</title></head>\n";
   }

   private function display_body() {
      echo "<body>$this->content</body>\n";
   }

   private function display_footer() {
      echo "</html>";
   }
}?>

And a sub classes that displays specific content:

<?php
class page_with_content extends page {

   function display() {
      $this->display_header();
      $this->get_content($arg);
      $this->display_body();
      $this->display_footer();
   }

   function get_content($arg) {
      //Gets special content here, possibly database calls
      $this->content = $content_from_above_db_calls;
   }
}?>

How do I implement a factory or reflection to dynamically get instances of different pages?

Link to comment
Share on other sites

I'll start with a factory which is easiest to implement.  We can use reflection if you're interested afterwards, but that involves using the Reflection API which I think is slightly confusing and poorly documented.  Factories can be very simple, but you can make them more sophisticated, say by adding reflection to them.

 

// Sample factory -- in this example I am going to use the PEAR naming convention
// e.g. if a class is in utils/ it's name starts with Utils_
// for example Utils_HashMap would be the class name residing in the file utils/HashMap.php
class PageFactory {
/**
 * Return a new instance of specified class
 * 
 * @param String $classname
 * @return PageController
 */
 public static function construct($classname)
 {
 	if !((class_exists($classname) || interface_exists($classname)))
 	{
		$root = dirname(__FILE__); // current directory - assuming it's the root
		$path = str_replace("_", DIRECTORY_SEPARATOR, $classname);

		require $root . DIRECTORY_SEPARATOR . strtolower(dirname($path)) . 
				DIRECTORY_SEPARATOR . basename($path) . ".php";
 	}
 	return new $classname;
 }
}

 

The above may in fact be using reflection underneath the hood when I say "new $classname", but I wanted to give you something slightly more sophisticated than a hashmap to look at.  In the example I convert the class name into a file path and require the file if the class definition is absent.  After that I return a new instance of the class.

 

Now if you don't implement the PEAR naming convention this won't work for you, so you might instead create a hash:

classnames => file paths

Then when a class name is passed in you could check whether it's been included, and if not require it.  It would be equivalent to what's going on above except there was no rule based way to go from a class name to a file path.  Really, we can introduce more explicit reflection, and I think we shoud, but the real problem will be including the files.

 

If you'd like to talk about strategies to include files, I can share what I do.  Including files ends up affecting how I create factories (like the one above), including things such as 3rd party libraries, and templates, and manage dependencies.  Sadly PHP doesn't have a very good way to include files so we're left to reinvent the wheel.

Link to comment
Share on other sites

Ok, this is a much higher level design than what I'm doing (or have ever done, for that matter). Let me see if I can't walk you through it to make sure I've got the logic correct. (And to clarify for anyone that reads this later.)

 

First, create a factory class:

class PageFactory { ... }

Define a constructor function that is invoked when the PageFactory class is instanced:

public static function construct($classname) { ... }

If the string (class) passed via $classname has not been defined or implemented:

if !((class_exists($classname) || interface_exists($classname))) { ... }

Deconstruct the classname based on the PEAR naming scheme, to set the path to the class:

$root = dirname(__FILE__); // current directory - assuming it's the root
$path = str_replace("_", DIRECTORY_SEPARATOR, $classname);

Require the class:

require $root . DIRECTORY_SEPARATOR . strtolower(dirname($path)) .
     DIRECTORY_SEPARATOR . basename($path) . ".php";

And return an new instance of the class:

return new $classname;

 

So, if we wanted to implement the factory to display our pages it would be invoked like this:

<?php
require_once('PageFactory.php');
$page = new PageFactory('Pages_DatabaseContent');
// Where, using the PEAR naming convention, the DatabaseContent class would be located 
// in /usr/local/www/pages/DatabaseContent.php assuming that the document root is
// /usr/local/www/

// Call the display or execute function in the DatabaseContent class
$page->execute();
?>

Is that right?

 

Very interesting, so essentially, the factory interprets the argument, finds and requires the correct file containing the class and hands off to the class specified in the argument.

 

I would be very interested to hear how you manage include files. An efficient system would allow for much modularity and encapsulation.

 

Thank you again for your insights!

Link to comment
Share on other sites

Close, except you would call the factory like so (because it's a static function):

$page = Page_Factory::construct("Pages_Login");

In this example you would get a class from the pages/Login.php file.

 

I have three things I use to include files.  First I follow the PEAR naming convention so that I can programmatically go between class names and file paths.  Second I use a set of functions I've made to include files once and only once (without using require_once which is inefficient).  Lastly, I use what I call Catalogs to get the file paths for things which do not follow the naming convention.  For instance, third party libraries, HTML/PHP templates, database files (if I'm using sqlite), and resource files.  I'll include the code for both the include manager and catalog.

 

Include Manager

<?php
/**
* Constants including files
*
* <p>Defines the default php file extension type, and the default
* directory separator.
*/
define('JUNCTION_EXTENSION', '.php');
define('JUNCTION_UNDERSCORE', '_');
define('JUNCTION_EMPTY_STRING', "");

/**
* Include the file for a given class only if the class 
* definition is absent
* 
* <p>This function assumes that the class names follow the
* PEAR naming conventions.
*
* @param String $classname
* @return void
*/
function using($classname) {
// PHP... why must you make this distinction?
if (class_exists($classname) || interface_exists($classname))
	return;
require(classtofile($classname));
}

/**
* Return whether a given file exists
* 
* <p>This function assumes that the class names follow the
* PEAR naming convetion.
*
* @param String $classname
* @return boolean
*/
function exists($classname) {
return file_exists(classtofile($classname));
}

/**
* Include the specified file and ensure that it is only
* included once.
* 
* <p>Note that the extension for the file should be omitted
* and will be appended by the function.
* <p>Use this function when you want to include a class but
* it does not follow the PEAR naming convention.
*
* @param String $file fully qualified path to a file
*/
function get($file) {
static $record = array();
if (!$record[$file]) {
	require($file . JUNCTION_EXTENSION);
	$record[$file] = true;
}
}

/**
* Includes an entire package if each class 
* definition is absent.
* 
* <p>This works similar to import in java.
* 
* @param string package
*/
function package($package)
{
static $packages;
if (!$packages[$package]) {
	$packages[$package] = true;
	$root = dirname(__FILE__);
	$path = str_replace("_", DIRECTORY_SEPARATOR, $package);

	$fullpath = $root . DIRECTORY_SEPARATOR . strtolower($path);

	if ($handle = opendir($fullpath)) {
		while (false !== ($file = readdir($handle))) {
			$file = $fullpath . DIRECTORY_SEPARATOR . $file;
			$classname = filetoclass($file);
			if(!is_dir($file) && !(class_exists($classname) || interface_exists($classname))) {
				require($file);
			}
		}
		closedir($handle);
	}
}
}

/**
* Take a class name and return the absolute path to the file containing it.
* 
* <p>This only works for classes following the PEAR naming convention.
* 
* @param string class name
* @return string
*/
function classtofile($classname) {
$root = dirname(__FILE__);
$path = str_replace("_", DIRECTORY_SEPARATOR, $classname);

return $root . DIRECTORY_SEPARATOR . strtolower(dirname($path)) . 
				DIRECTORY_SEPARATOR . basename($path) . JUNCTION_EXTENSION;
}

/**
* Take a file name and return the class which resides in the file
* 
* <p>This only works for classes following the PEAR naming convention.
* It will work for both files with an absolute path and those relative
* to the root directory.
*/
function filetoclass($file) {
$root = dirname(__FILE__);
$file = str_replace($root, "", $file);
$class = basename($file, JUNCTION_EXTENSION);
$dir = trim(dirname($file), "/");
$parts = explode(DIRECTORY_SEPARATOR, $dir);
$classname = "";
foreach ($parts as $part) {
	$classname .= ucfirst($part) . '_';
}
return $classname . $class;
}
?>

 

Here is a sample catalog, it's stupidly simple:

<?php
/**
* Retrieve the full path to the desired resource
*
* @package junction.utils
* @author Alexander Schearer <aas11@duke.edu>
*/
class Resources_Catalog {

public static function fetch($resource) {
	return dirname(__FILE__) . DIRECTORY_SEPARATOR . $resource;
}
}
?>

 

So, if I have a class with dependencies I would write:

<?php
package("Utils");
using("Libs_Catalog");
get(Libs_Catalog::fetch("adodb/adodb.inc");

class Database_Abstraction_Object {
...
}
?>

This would include all the utilities, the library catalog, and the adodb library.  Just remember that the file containing the include functions needs to be included at some point, and it must be located in the top level directory of your project in order to work.  I usually have the first script which is called by the server require the include manager first and then use its functions throughout the rest of my code.

 

Let me know if you have any questions, or want to discuss other issues or techniques.

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.