Is there a simple way to log to queries to a file? I have Firebug profiling working no problem with:
resources.db.params.profiler.enabled = "true"
resources.db.params.profiler.class = "Zend_Db_Profiler_Firebug"
It would be nice to log this to a file with out writing a bunch of code.
Is there a class I can swap out with Zend_Db_Profiler_Firebug?
UPDATE: See my answer below.
As of ZF 1.11.11 there is no built in profiler class that will log queries to a file. Currently, FireBug is the only specialized Db Profiler.
Here are two ways in which you can solve it without loads of extra code though.
First, check out this answer as it shows how to extend Zend_Db_Profiler to have it log the queries to a file on queryEnd. If it doesn't quite do what you want, you can extend Zend_Db_Profiler and use the provided code as a starting point.
This next example is a slight modification of a plugin I have in some of my applications that I use to profile queries when the application is in development. This method utilizes a dispatchLoopShutdown() plugin to get an instance of the Db Profiler and log the queries to a file.
<?php /* library/My/Page/DbLogger.php */
class My_Page_DbLogger extends Zend_Controller_Plugin_Abstract
{
public function dispatchLoopShutdown()
{
$db = Zend_Controller_Front::getInstance()->getParam('bootstrap')->getResource('db');
$profiler = $db->getProfiler();
if ($profiler === NULL || !($profiler instanceof Zend_Db_Profiler))
return;
// either create your logger here based on config in application.ini
// or create it elsewhere and store it in the registry
$logger = Zend_Registry::get('dblog');
$totalQueries = $profiler->getTotalNumQueries();
$queryTime = $profiler->getTotalElapsedSecs();
$longestTime = 0;
$queries = $profiler->getQueryProfiles();
if ($queries !== false) {
$content = "\nExecuted $totalQueries database queries in $queryTime seconds<br />\n";
foreach ($queries as $query) {
// TODO: You could use custom logic here to log only selected queries
$content .= "Query (" . $query->getElapsedSecs() . "s): " . $query->getQuery() . "\n";
if ($query->getElapsedSecs() > $longestTime) {
$longestTime = $query->getElapsedSecs();
}
}
$content .= "Longest query time: $longestTime.\n" . str_repeat('-', 80);
$logger->info($content);
}
}
}
To activate this plugin, you can use code like this in your bootstrap:
/**
* Register the profiler if we are running in a non-production mode
*/
protected function _initPageProfiler()
{
if (APPLICATION_ENV == 'development') {
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new My_Page_DbLogger());
}
}
Ideally, in the long term, you would probably want to make a class that extends Zend_Db_Profiler and allow additional options to be specified in your config such as the log file path, the log priority. This way you can leverage the existing filters to Zend_Db_Profiler.
Related
The name is quite bad, but I really don't know what else to call it.
I'm trying to make a extendable and modular plugin system for my website. I need to be able to access plugin php files that exist in a plugin directory and get access to their classes to call functions such as getting the html content that the plugin should show and more.
Below is a semi-pseudo code example of what I am trying to achieve, but how to actually arbitrarily load the plugins is where I am stuck (PluginLoader.php).
-Max
//BasePlugin.php
abstract class BasePlugin
{
public function displayContent()
{
print "<p>Base Plugin</p>";
}
};
//ExamplePlugin.php -> In specific plugin directory.
require('../BasePlugin.php');
class ExamplePlugin extends BasePlugin
{
public static function Instance()
{
static $inst = null;
if ($inst === null) {
$inst = new ExamplePlugin();
}
return $inst;
}
public function displayContent()
{
print "<p>Example Plugin</p>";
}
}
//PluginLoader.php
foreach($pluginFile : PluginFilesInDirectory) { // Iterate over plugin php files in plugin directory
$plugin = GetPlugin($pluginFile); // Somehow get instance of plugin.
echo plugin->displayContent();
}
I'm guessing here, but it seems to me that you need to:
get a list of the plugins in the desired directory.
include or require the plugin's class file.
create an instance of the class.
call the plugin's displayContent() method.
So, you probably want to do something like
$pluginDir = 'your/plugin/directory/' ;
$plugins = glob($pluginDir . '*.php') ;
foreach($plugins as $plugin) {
// include the plugin file
include_once($plugin) ;
// grab the class name from the plugin's file name
// this finds the last occurrence of a '/' and gets the file name without the .php
$className = substr($plugin,strrpos($plugin,'/') + 1, -4) ;
// create the instance and display your test
$aPlugin = $className::Instance() ;
$aPlugin->displayContent() ;
}
There's probably a cleaner way to do it, but that will ready your directory, get the plugins' code, and instantiate each one. How you manage/reference them afterwards depends on how your plugins register with your application.
I have build a CMS using Zend Framework (1.11). In the application I have two modules, one called 'cms' which contains all the CMS logic and an other 'web' which enables a user to build their own website around the CMS. This involves adding controllers/views/models etc all in that module.
The application allows you to serve multiple instances of the app with their own themes. These instances are identified by the hostname. During preDispatch(), a database lookup is done on the hostname. Based on the database field 'theme' the app then loads the required css files and calls Zend_Layout::setLayout() to change the layout file for that specific instance.
I want to extend this functionality to also allow the user to run different web modules based on the 'theme' db field. However, this is where I am stuck. As it is now, the web module serves the content for all the instances of the application.
I need the application to switch to a different web module based on the 'theme' database value (so indirectly the hostname). Any ideas?
Well, in my opinion,
You should write a front controller plugin for the web module, and do it so, that when you need another plugin, you can do so easily.
The front controller plugin should look something like this:
class My_Controller_Plugin_Web extends My_Controller_Plugin_Abstract implements My_Controller_Plugin_Interface
{
public function init()
{
// If user is not logged in - send him to login page
if(!Zend_Auth::getInstance()->hasIdentity()) {
// Do something
return false;
} else {
// You then take the domain name
$domainName = $this->_request->getParam( 'domainName', null );
// Retrieve the module name from the database
$moduleName = Module_fetcher::getModuleName( $domainName );
// And set the module name of the request
$this->_request->setModuleName( $moduleName );
if(!$this->_request->isDispatched()) {
// Good place to alter the route of the request further
// the way you want, if you want
$this->_request->setControllerName( $someController );
$this->_request->setActionName( $someAction );
$this->setLayout( $someLayout );
}
}
}
/**
* Set up layout
*/
public function setLayout( $layout )
{
$layout = Zend_Layout::getMvcInstance();
$layout->setLayout( $layout );
}
}
And the My_Controller_Plugin_Abstract, which is not an actual abstract and which your controller plugin extends,looks like this:
class My_Controller_Plugin_Abstract
{
protected $_request;
public function __construct($request)
{
$this->setRequest($request);
$this->init();
}
private function setRequest($request)
{
$this->_request = $request;
}
}
And in the end the front controller plugin itself, which decides which one of the specific front controller plugins you should execute.
require_once 'Zend/Controller/Plugin/Abstract.php';
require_once 'Zend/Locale.php';
require_once 'Zend/Translate.php';
class My_Controller_Plugin extends Zend_Controller_Plugin_Abstract
{
/**
* before dispatch set up module/controller/action
*
* #param Zend_Controller_Request_Abstract $request
*/
public function routeShutdown(Zend_Controller_Request_Abstract $request)
{
// Make sure you get something
if(is_null($this->_request->getModuleName())) $this->_request->setModuleName('web');
// You should use zend - to camelCase convertor when doing things like this
$zendFilter = new Zend_Filter_Word_SeparatorToCamelCase('-');
$pluginClass = 'My_Controller_Plugin_'
. $zendFilter->filter($this->_request->getModuleName());
// Check if it exists
if(!class_exists($pluginClass)) {
throw new Exception('The front controller plugin class "'
. $pluginClass. ' does not exist');
}
Check if it is written correctly
if(!in_array('My_Controller_Plugin_Interface', class_implements($pluginClass))) {
throw new Exception('The front controller plugin class "'
. $pluginClass.'" must implement My_Controller_Plugin_Interface');
}
// If all is well instantiate it , and you will call the construct of the
// quasi - abstract , which will then call the init method of your
// My_Plugin_Controller_Web :)
$specificController = new $pluginClass($this->_request);
}
}
If you have never done this, now is the time. :)
Also, to register your front controller plugin with the application, you should edit the frontController entry in your app configuration. I will give you a json example, i'm sure you can translate it to ini / xml / yaml if you need...
"frontController": {
"moduleDirectory": "APPLICATION_PATH/modules",
"defaultModule": "web",
"modules[]": "",
"layout": "layout",
"layoutPath": "APPLICATION_PATH/layouts/scripts",
// This is the part where you register it!
"plugins": {
"plugin": "My_Controller_Plugin"
}
This might be a tad confusing, feel free to ask for a more detailed explanation if you need it.
I am writing an CSV/Excel-->MySQL import manager for an MVC application (Kohana/PHP).
I have a controller named "ImportManager" which has an action named "index" (default) which displays in a grid all the valid .csv and .xls files that are in a specific directory and ready for import. The user can then choose the files he wants to import.
However, since .csv files import into one database table and .xls files import into multiple database tables, I needed to handle this abstraction. Hence I created a helper class called SmartImportFile to which I send each file be it .csv or .xls and then I get then ask this "smart" object to add the worksheets from that file (be they one or many) to my collection. Here is my action method in PHP code:
public function action_index()
{
$view = new View('backend/application/importmanager');
$smart_worksheets = array();
$raw_files = glob('/data/import/*.*');
if (count($raw_files) > 0)
{
foreach ($raw_files as $raw_file)
{
$smart_import_file = new Backend_Application_Smartimportfile($raw_file);
$smart_worksheets = $smart_import_file->add_smart_worksheets_to($smart_worksheets);
}
}
$view->set('smart_worksheets', $smart_worksheets);
$this->request->response = $view;
}
The SmartImportFile class looks like this:
class Backend_Application_Smartimportfile
{
protected $file_name;
protected $file_extension;
protected $file_size;
protected $when_file_copied;
protected $file_name_without_extension;
protected $path_info;
protected $current_smart_worksheet = array();
protected $smart_worksheets = array();
public function __construct($file_name)
{
$this->file_name = $file_name;
$this->file_name_without_extension = current(explode('.', basename($this->file_name)));
$this->path_info = pathinfo($this->file_name);
$this->when_file_copied = date('Y-m-d H:i:s', filectime($this->file_name));
$this->file_extension = strtolower($this->path_info['extension']);
$this->file_extension = strtolower(pathinfo($this->file_name, PATHINFO_EXTENSION));
if(in_array($this->file_extension, array('csv','xls','xlsx')))
{
$this->current_smart_worksheet = array();
$this->process_file();
}
}
private function process_file()
{
$this->file_size = filesize($this->file_name);
if(in_array($this->file_extension, array('xls','xlsx')))
{
if($this->file_size < 4000000)
{
$this->process_all_worksheets_of_excel_file();
}
}
else if($this->file_extension == 'csv')
{
$this->process_csv_file();
}
}
private function process_all_worksheets_of_excel_file()
{
$worksheet_names = Import_Driver_Excel::get_worksheet_names_as_array($this->file_name);
if (count($worksheet_names) > 0)
{
foreach ($worksheet_names as $worksheet_name)
{
$this->current_smart_worksheet['name'] = basename($this->file_name).' ('.$worksheet_name.')';
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension.'__'.$worksheet_name;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
}
}
private function process_csv_file()
{
$this->current_smart_worksheet['name'] = basename($this->file_name);
$this->current_smart_worksheet['kind'] = strtoupper($this->file_extension);
$this->current_smart_worksheet['file_size'] = $this->file_size;
$this->current_smart_worksheet['when_file_copied'] = $this->when_file_copied;
$this->current_smart_worksheet['table_name'] = $this->file_name_without_extension;
$this->assign_database_table_fields();
$this->smart_worksheets[] = $this->current_smart_worksheet;
}
private function assign_database_table_fields()
{
$db = Database::instance('import_excel');
$sql = "SHOW TABLE STATUS WHERE name = '".$this->current_smart_worksheet['table_name']."'";
$result = $db->query(Database::SELECT, $sql, FALSE)->as_array();
if(count($result))
{
$when_table_created = $result[0]['Create_time'];
$when_file_copied_as_date = strtotime($this->when_file_copied);
$when_table_created_as_date = strtotime($when_table_created);
if($when_file_copied_as_date > $when_table_created_as_date)
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoreimport';
}
else
{
$this->current_smart_worksheet['status'] = 'backend.application.import.status.isuptodate';
}
$this->current_smart_worksheet['when_table_created'] = $when_table_created;
}
else
{
$this->current_smart_worksheet['when_table_created'] = 'backend.application.import.status.tabledoesnotexist';
$this->current_smart_worksheet['status'] = 'backend.application.import.status.needtoimport';
}
}
public function add_smart_worksheets_to(Array $smart_worksheets = array())
{
return array_merge($smart_worksheets, $this->get_smart_worksheets());
}
public function get_smart_worksheets()
{
if ( ! is_array($this->smart_worksheets))
{
return array();
}
return $this->smart_worksheets;
}
}
In a code review I was told that it might be better not to have a helper class like this but to keep the bulk of the code in the controller action method itself. The argumentation was that you should be able to look at the code in a controller action and see what it does instead of having it call external helper classes outside of itself. I disagree. My argumentation is:
you should create a helper class anytime it makes code clearer, as in this case, it abstracts away the fact that some files have one worksheet or many worksheets in them, and allows for easy future extension, if, say, we want to also import from sqlite files or even directories with files in them, this class abstraction would be able to handle this nicely.
moving the bulk of the code from this helper class back into the controller would force me to create internal variables in the controller which make sense for this particular action, but may or may not make sense to other action methods within the controller.
if I were programming this in C# I would make this helper class a nested class which would literally be an internal data structure that is inside of and only available to the controller class, but since PHP does not allow nested classes, I need to call a class "outside" the controller to help manage this abstraction in a way that makes the code clear and readable
Based on your experience of programming in the MVC pattern, should the above helper class be refactored back into the controller or not?
There are two approaches to controllers: make it thin or thick. When I started my adventure with MVC I made a mistake of creating thick controllers - now I prefer make it as thin as possible. Your solution is good in my opinion.
Here is how I would redesigned your code even further:
class Backend_Application_SmartImport {
public function __construct( $raw_files ) {
}
public function process() {
foreach ($raw_files as $raw_file) {
// (...)
$oSmartImportFileInstance = $this->getSmartImportFileInstance( $smart_import_file_extension );
}
}
protected function getSmartImportFileInstance( $smart_import_file_extension ) {
switch ( $smart_import_file_extension ) {
case 'xml':
return new Backend_Application_SmartImportFileXml();
// (...)
}
}
}
abstract class Backend_Application_SmartImportFile {
// common methods for importing from xml or cvs
abstract function process();
}
class Backend_Application_SmartImportFileCVS extends Backend_Application_SmartImportFile {
// methods specified for cvs importing
}
class Backend_Application_SmartImportFileXls extends Backend_Application_SmartImportFile {
// methods specified for xls importing
}
The idea is to have two classes responsible for processing xml and cvs inheriting from a base class. The main class uses a special method to detect how the data should be processed (Strategy Pattern). The controller just passed a list of files to the instance of Backend_Application_SmartImport class and passes result of process method to the view.
The advantage of my solution is that code is more decoupled and you can easily and in a clean way add new types of processing like xml, pdf, etc.
I agree with you Edward.
Your ImportController does what a Controller is meant to do. It generates the list of files for the user to view and act on, it then passes that list to the View for it to display. I am presuming that you have a process action or similar which is handles the request when a user has selected a file, this file is then passed on to the Helper in question.
The Helper is a perfect example of abstraction and entirely justified in its usage and existence. It is not coupled with the Controller in anyway and doesn't need to be. The Helper could be easily used in other scenarios where the Controller is not present, for example a CRON task, a public API which users can call programmatically without your ImportController.
Your right on the ball with this one. Stick it to 'em!
I'm trying to access an FTP server from my PHP script using Codeigniter's FTP Library. These functions work great, but when testing the script I discovered that if I attempt to connect to a server that does not exist, the script does not terminate with an error message of any kind.
The page continues to execute, until the web server gives up, returning an empty document.
So I am wondering, is there a way to limit the amount of time that Codeigniter can try to connect to an FTP server, then display a message if that times out?
I tried using the php function set_time_limit(), but it does not behave how I expected it to.
Thanks for your help.
Codeigniter's ftp class uses the underlying ftp_connect php call that supports a 3rd optional parameter, timeout (http://ca2.php.net/manual/en/function.ftp-connect.php).
Codeigniter however does not use it, but allows for extending the default libraries it provides (providing that you're willing to do some work and check that any updates you do to the core will not break the functionality of your extended class). So to solve your problem you could create a new library in you application library folder:
<?php
class MY_FTP extends CI_FTP { //Assuming that in your config.php file, your subclass prefix is set to 'MY_' like so: $config['subclass_prefix'] = 'MY_';
var $timeout = 90;
/**
* FTP Connect
*
* #access public
* #param array the connection values
* #return bool
*/
function connect($config = array())
{
if (count($config) > 0)
{
$this->initialize($config);
}
if (FALSE === ($this->conn_id = ftp_connect($this->hostname, $this->port, $this->timeout)))
{
if ($this->debug == TRUE)
{
$this->_error('ftp_unable_to_connect');
}
return FALSE;
}
if ( ! $this->_login())
{
if ($this->debug == TRUE)
{
$this->_error('ftp_unable_to_login');
}
return FALSE;
}
// Set passive mode if needed
if ($this->passive == TRUE)
{
ftp_pasv($this->conn_id, TRUE);
}
return TRUE;
}
}
?>
and from your script, you could add to your configuration array the timeout option:
$this->load->library('ftp'); //if ftp is not autoloaded
$ftp_params = array('hostname'=>'1.2.3.4', 'port'=>21, 'timeout'=>10); //timout is 10 seconds instead of default 90
$ftp_conn = $this->ftp->connect($ftp_params);
if(FALSE === $ftp_conn) {
//Code to handle error
}
The ftp class is not designed to give error messages unless the debug parameter is set to TRUE in te config array, in which case it'll just display an error. However it can also be override, because all errors call the function _error() in the class. So you could set 'debug' => true in your $ftp_params array, and add a function in MY_ftp like so:
/**
* This function overrides
*/
function _error($line)
{
$this->error = $line;
}
And then have a function getError()
/**
* This function overrides
*/
function get_error()
{
return $this->error;
}
So if
$ftp_conn = $this->ftp->connect($ftp_params);
returns false, you can call
$error = $this->ftp->get_error();
to get your error and display it.
Now, you can always customize and have a more complex error handling mechanism by further customizing the class...
Hope it answers your question.
The answer is simple, don't attempt to connect to a server that doesn't exist.
I am starting a new web application in PHP and this time around I want to create something that people can extend by using a plugin interface.
How does one go about writing 'hooks' into their code so that plugins can attach to specific events?
You could use an Observer pattern. A simple functional way to accomplish this:
<?php
/** Plugin system **/
$listeners = array();
/* Create an entry point for plugins */
function hook() {
global $listeners;
$num_args = func_num_args();
$args = func_get_args();
if($num_args < 2)
trigger_error("Insufficient arguments", E_USER_ERROR);
// Hook name should always be first argument
$hook_name = array_shift($args);
if(!isset($listeners[$hook_name]))
return; // No plugins have registered this hook
foreach($listeners[$hook_name] as $func) {
$args = $func($args);
}
return $args;
}
/* Attach a function to a hook */
function add_listener($hook, $function_name) {
global $listeners;
$listeners[$hook][] = $function_name;
}
/////////////////////////
/** Sample Plugin **/
add_listener('a_b', 'my_plugin_func1');
add_listener('str', 'my_plugin_func2');
function my_plugin_func1($args) {
return array(4, 5);
}
function my_plugin_func2($args) {
return str_replace('sample', 'CRAZY', $args[0]);
}
/////////////////////////
/** Sample Application **/
$a = 1;
$b = 2;
list($a, $b) = hook('a_b', $a, $b);
$str = "This is my sample application\n";
$str .= "$a + $b = ".($a+$b)."\n";
$str .= "$a * $b = ".($a*$b)."\n";
$str = hook('str', $str);
echo $str;
?>
Output:
This is my CRAZY application
4 + 5 = 9
4 * 5 = 20
Notes:
For this example source code, you must declare all your plugins before the actual source code that you want to be extendable. I've included an example of how to handle single or multiple values being passed to the plugin. The hardest part of this is writing the actual documentation which lists what arguments get passed to each hook.
This is just one method of accomplishing a plugin system in PHP. There are better alternatives, I suggest you check out the WordPress Documentation for more information.
So let's say you don't want the Observer pattern because it requires that you change your class methods to handle the task of listening, and want something generic. And let's say you don't want to use extends inheritance because you may already be inheriting in your class from some other class. Wouldn't it be great to have a generic way to make any class pluggable without much effort? Here's how:
<?php
////////////////////
// PART 1
////////////////////
class Plugin {
private $_RefObject;
private $_Class = '';
public function __construct(&$RefObject) {
$this->_Class = get_class(&$RefObject);
$this->_RefObject = $RefObject;
}
public function __set($sProperty,$mixed) {
$sPlugin = $this->_Class . '_' . $sProperty . '_setEvent';
if (is_callable($sPlugin)) {
$mixed = call_user_func_array($sPlugin, $mixed);
}
$this->_RefObject->$sProperty = $mixed;
}
public function __get($sProperty) {
$asItems = (array) $this->_RefObject;
$mixed = $asItems[$sProperty];
$sPlugin = $this->_Class . '_' . $sProperty . '_getEvent';
if (is_callable($sPlugin)) {
$mixed = call_user_func_array($sPlugin, $mixed);
}
return $mixed;
}
public function __call($sMethod,$mixed) {
$sPlugin = $this->_Class . '_' . $sMethod . '_beforeEvent';
if (is_callable($sPlugin)) {
$mixed = call_user_func_array($sPlugin, $mixed);
}
if ($mixed != 'BLOCK_EVENT') {
call_user_func_array(array(&$this->_RefObject, $sMethod), $mixed);
$sPlugin = $this->_Class . '_' . $sMethod . '_afterEvent';
if (is_callable($sPlugin)) {
call_user_func_array($sPlugin, $mixed);
}
}
}
} //end class Plugin
class Pluggable extends Plugin {
} //end class Pluggable
////////////////////
// PART 2
////////////////////
class Dog {
public $Name = '';
public function bark(&$sHow) {
echo "$sHow<br />\n";
}
public function sayName() {
echo "<br />\nMy Name is: " . $this->Name . "<br />\n";
}
} //end class Dog
$Dog = new Dog();
////////////////////
// PART 3
////////////////////
$PDog = new Pluggable($Dog);
function Dog_bark_beforeEvent(&$mixed) {
$mixed = 'Woof'; // Override saying 'meow' with 'Woof'
//$mixed = 'BLOCK_EVENT'; // if you want to block the event
return $mixed;
}
function Dog_bark_afterEvent(&$mixed) {
echo $mixed; // show the override
}
function Dog_Name_setEvent(&$mixed) {
$mixed = 'Coco'; // override 'Fido' with 'Coco'
return $mixed;
}
function Dog_Name_getEvent(&$mixed) {
$mixed = 'Different'; // override 'Coco' with 'Different'
return $mixed;
}
////////////////////
// PART 4
////////////////////
$PDog->Name = 'Fido';
$PDog->Bark('meow');
$PDog->SayName();
echo 'My New Name is: ' . $PDog->Name;
In Part 1, that's what you might include with a require_once() call at the top of your PHP script. It loads the classes to make something pluggable.
In Part 2, that's where we load a class. Note I didn't have to do anything special to the class, which is significantly different than the Observer pattern.
In Part 3, that's where we switch our class around into being "pluggable" (that is, supports plugins that let us override class methods and properties). So, for instance, if you have a web app, you might have a plugin registry, and you could activate plugins here. Notice also the Dog_bark_beforeEvent() function. If I set $mixed = 'BLOCK_EVENT' before the return statement, it will block the dog from barking and would also block the Dog_bark_afterEvent because there wouldn't be any event.
In Part 4, that's the normal operation code, but notice that what you might think would run does not run like that at all. For instance, the dog does not announce it's name as 'Fido', but 'Coco'. The dog does not say 'meow', but 'Woof'. And when you want to look at the dog's name afterwards, you find it is 'Different' instead of 'Coco'. All those overrides were provided in Part 3.
So how does this work? Well, let's rule out eval() (which everyone says is "evil") and rule out that it's not an Observer pattern. So, the way it works is the sneaky empty class called Pluggable, which does not contain the methods and properties used by the Dog class. Thus, since that occurs, the magic methods will engage for us. That's why in parts 3 and 4 we mess with the object derived from the Pluggable class, not the Dog class itself. Instead, we let the Plugin class do the "touching" on the Dog object for us. (If that's some kind of design pattern I don't know about -- please let me know.)
The hook and listener method is the most commonly used, but there are other things you can do. Depending on the size of your app, and who your going to allow see the code (is this going to be a FOSS script, or something in house) will influence greatly how you want to allow plugins.
kdeloach has a nice example, but his implementation and hook function is a little unsafe. I would ask for you to give more information of the nature of php app your writing, And how you see plugins fitting in.
+1 to kdeloach from me.
Here is an approach I've used, it's an attempt to copy from Qt signals/slots mechanism, a kind of Observer pattern.
Objects can emit signals.
Every signal has an ID in the system - it's composed by sender's id + object name
Every signal can be binded to the receivers, which simply is a "callable"
You use a bus class to pass the signals to anybody interested in receiving them
When something happens, you "send" a signal.
Below is and example implementation
<?php
class SignalsHandler {
/**
* hash of senders/signals to slots
*
* #var array
*/
private static $connections = array();
/**
* current sender
*
* #var class|object
*/
private static $sender;
/**
* connects an object/signal with a slot
*
* #param class|object $sender
* #param string $signal
* #param callable $slot
*/
public static function connect($sender, $signal, $slot) {
if (is_object($sender)) {
self::$connections[spl_object_hash($sender)][$signal][] = $slot;
}
else {
self::$connections[md5($sender)][$signal][] = $slot;
}
}
/**
* sends a signal, so all connected slots are called
*
* #param class|object $sender
* #param string $signal
* #param array $params
*/
public static function signal($sender, $signal, $params = array()) {
self::$sender = $sender;
if (is_object($sender)) {
if ( ! isset(self::$connections[spl_object_hash($sender)][$signal])) {
return;
}
foreach (self::$connections[spl_object_hash($sender)][$signal] as $slot) {
call_user_func_array($slot, (array)$params);
}
}
else {
if ( ! isset(self::$connections[md5($sender)][$signal])) {
return;
}
foreach (self::$connections[md5($sender)][$signal] as $slot) {
call_user_func_array($slot, (array)$params);
}
}
self::$sender = null;
}
/**
* returns a current signal sender
*
* #return class|object
*/
public static function sender() {
return self::$sender;
}
}
class User {
public function login() {
/**
* try to login
*/
if ( ! $logged ) {
SignalsHandler::signal(this, 'loginFailed', 'login failed - username not valid' );
}
}
}
class App {
public static function onFailedLogin($message) {
print $message;
}
}
$user = new User();
SignalsHandler::connect($user, 'loginFailed', array($Log, 'writeLog'));
SignalsHandler::connect($user, 'loginFailed', array('App', 'onFailedLogin'));
$user->login();
?>
I believe the easiest way would be to follow Jeff's own advice and have a look around the existing code. Try looking at WordPress, Drupal, Joomla, and other well-known PHP-based CMS to see how their API hooks look and feel. This way you can even get ideas you may have not thought of previously to make things a little more robust.
A more direct answer would be to write general files that they would "include_once" into their file that would provide the usability they would need. This would be broken up into categories and NOT provided in one MASSIVE "hooks.php" file. Be careful though, because what ends up happening is that files that they include end up having more and more dependencies and functionality improves. Try to keep API dependencies low. I.E fewer files for them to include.
There's a neat project called Stickleback by Matt Zandstra at Yahoo that handles much of the work for handling plugins in PHP.
It enforces the interface of a plugin class, supports a command line interface and isn't too hard to get up and running - especially if you read the cover story about it in the PHP architect magazine.
Good advice is to look how other projects have done it. Many call for having plugins installed and their "name" registered for services (like wordpress does) so you have "points" in your code where you call a function that identifies registered listeners and executes them. A standard OO design patter is the Observer Pattern, which would be a good option to implement in a truly object oriented PHP system.
The Zend Framework makes use of many hooking methods, and is very nicely architected. That would be a good system to look at.
I am surprised that most of the answers here seem to be geared about plugins that are local to the web application, ie, plugins that run on the local web server.
What about if you wanted the plugins to run on a different - remote - server? The best way to do this would be to provide a form that allows you to define different URLs that would be called when particular events occur in your application.
Different events would send different information based on the event that just occurred.
This way, you would just perform a cURL call to the URL that has been provided to your application (eg over https) where remote servers can perform tasks based on information that has been sent by your application.
This provides two benefits:
You don't have to host any code on your local server (security)
The code can be on remote servers (extensibility) in different languages other then PHP (portability)