Wednesday, December 9, 2009

Cleaning/restoring using helper objects

Just wanted to show all of you who use OO PHP a technique that is quite familiar to old-time C++ programmers, but generally not so well known in the PHP world (think auto_ptr). This technique allows for much cleaner code in scenarios where you have to allocate a resource (or change global state) and have to cleanup before exiting the (function/method) scope.

I first applied this when I had a method in a class that needed to execute a function that might create an error condition that I wanted to intercept because I needed to do some stuff before letting the exception escape. The classic PHP approach would be something like this:

The problem is that as written the error handler will not be restored if we (re) throw the exception. So we fix that:

But now we are starting to repeat the code. So here is what it looks like with the alternate, more object oriented technique:

So, where did the 'restore_error_handler' go? Now it won't be restored at all! Not so. Lets have a look at the ErrorHandler class:

Now, if we look at the last version of the 'dosomething' function above, you will notice that whether the function ends by throwing an exception, or because it returns normally, the '$handler' variable will go out of scope and will be destructed. The class' destructor function takes care of restoring the previous state. This approach allows you to forget about when/where to let go/restore of resources.

The technique can be generalized to any other kind of object that needs to clean up after itself. If you already have existing objects, make sure you have an appropriate '__destruct' method. The above illustrates how you can introduce some new objects to encapsulate create/restore behavior for non-object type functions.

WARNING PHP makes no guarantees about the order in which variables that go out of scope get destructed. This is generally not an issue, but if you were to, for example, create two handlers like the above in the same scope, they may get broken down in the opposite order from what you expect. Since 'restore_error_handler' carries no context, it doesn't matter in this case: as long as it gets done twice. It may not be true of other scenario's. Take this example:

Lets assume the error_reporting level is 0 before we execute 'test()'. Creating '$er1' will set it to 'E_ALL' (and remember 0), and then creating '$er2' will set it to 'E_WARN' (and remember 'E_ALL'). So far so good. We do some work and exit. If destruction happens in the order '$er2' and then '$er1' things are peachy and we end up with 0 as the error_reporting level. If destruction happens the other way around though, we end up with an error_reporting level of 'E_ALL'!

One simple way of getting around this problem is to have each object that implements this behavior hold a reference to other objects that should be destructed later:

Now '$er2' is holding a reference to '$er1'. If PHP wants to destruct '$er1' it founds it has a reference count of 2, so it is not considered (yet), but its count is reduced to 1. '$er2' has a reference count of 1, so it's '__destruct' function is called. It does what it needs to do, and then unsets the reference(s) it holds. This causes the reference count of '$er1' to go from 1 to 0 and '$er1' is finally destructed.

Saturday, November 14, 2009

Using Symfony dependency injection Container with Zend_Bootstrap

I recently converted an old(er) Zend Framework based application to the new Zend_Application/Zend_Bootstrap style bootstrap approach. In doing so, I of course started down the path of creating components (resources in Zend speak) that are more easily used for dependency injection approaches. As I did so, I found myself writing a fair amount of code in subclasses of existing resource loaders to create the setup(s) I wanted from configuration files. Surely there had to be a better way.

I encountered the Symfony dependency injection container which looks very promising. For one it is compatible with Zend's notion of what a container should be. Zend doesn't formalize the interface to a container, because the interface only depends on "magic" methods. Yet the Symfony DI container is compatible. I found an article by Benjamin Eberlei that shows how to do this. There was one thing I did not like about the solution: it requires modification by an otherwise basic index.php file.

The reason for the modification is that you need to use the setContainer method on the bootstrap instance to inject the container into the bootstrap. A typical index.php will contain the following code:


What we will need now, however:

The need to create the container inside the index.php was not very flexible to me (I like to develop a general code base that I can use over and over, and not all projects might use this approach). So here is what I did about it.

It all starts by using a custom bootstrap class (subclass of Zend_Application_Bootstrap_BootstrapAbstract). I have such a class anyway that I use to do various other things. The bootstrap class calls a set method on any top level element it encounters in the configuration. I added these two methods to my class:

As you can see, setContainer (called from setOptions when a 'containerfactory' element is present) will store a factory instance in the bootstrap object, where its class was specified as the 'factory' option in the configuration ($options). We've overridden the standard version of 'getContainer'. If no factory was defined, it behaves as before, but if the factory was installed, it is used to create the container. This makes this new bootstrap class completely independent from the specifics of how to actually create the container.

Now suppose we feed 'setContainerFactory' the following configuration:

Everything inside the 'containerFactory' element is passed on to the constructor of the factory. To understand the meaning of the options, let's look at the factory class itself. For now, ignore the 'flattenParameters' and 'getParameters' methods. They'll be explained later.

So, let's have a look at this. The constructor, which receives the piece of the configuration as an array, stores the configuration ($options) for later, and initializes the autoloader for the symfony component so that, later on, its classes may be found automatically. When it comes time for the bootstrap to create the container it is going to call 'makeContainer'. While we could have simply hard-coded the creation there, I have chosen to make it more flexible, again.

First off we extract some parameters from the configuration. 'config_file' contains the path to the (top level) configuration for the DI container. This can be any file in XML, YAML, or INI format. If that were it, we would just use the appropriate method to load the configuration and be done with it. Parsing any of these files formats is relatively expensive and we do not want to have it happen for every single request we serve. Symfony provides a very neat mechanism to avoid that.

Symfony is able to write out a configuration, once read in, in a variety of formats, but in particular, it can do so as a php file containing a custom container class. This class will be named after the 'class' parameter of the configuration, or will be named 'Container' by default. If the 'dump_file' parameter is defined it contains the path where this source code will have been generated and if the file exists, we simply include/require it. The class will now be loaded, so we can call 'new' and be done. Very fast!

If the file does not (yet) exist, we will use appropriate calls the Symfony functions to read the configuration (all formats supported), and the dump it to the dump file (if configured). If no dump_file was configured things still work, but we'll load (slowly) from the configuration every request.

With this approach in place, simply adding a 'containerfactory' element to the bootstrap configuration and setting it up appropriately (and creating the corresponding factory class), you can now hook in any kind of DI container, not just the Symfony one.

So what about this 'getParameters' method? If you check out the documentation for the Symfony DI container, you will find that while you can specify parameters in the configuration, it is also possible to specify defaults for any subset of them by passing those defaults to the builder. That is exactly what we're doing here, but why?

In the rest of my infrastructure code, not discussed here, I have build a facility to have any piece of an configuration file (passed to the application object), refer to other pieces of the file and have values substituted. It allows me to define certain items, essentially as constants, once in the configuration file, and reuse them in different places. With the introduction of a DI container with its own configuration, I did not want to have to repeat these constants in its configuration, and that is what is achieved here. The '<parameters>' section refers (using the notation $(values) to a section in the overall configuration called <values> '. The whole 'values' array is flattened into a one-dimensional array, concatenating array keys with period in between to make the format consistent with that used by Symfony. The end result is that the parameters from the main application configuration file make it as defaults into the Symfony DI container that was generated.

That's it. Nothing else needs changing. You create your DI configuration in XML, YAML or INI format (or a mix using imports) and off you go.

Monday, July 27, 2009

ZendStudio 6.x.x performance issue

I have occasionally been plagued by less than stellar performance while using ZendStudio 6.x.x series. It happened again the other day and I finally made some progress in tracking it down, so let me share.

What happened

Recently I had to work on a fairly large PHP project on my laptop for a while. My normal environment is a 8 core MacPro, the laptop is a Core2Due MacBookPro. In both cases I run VMWare Fusion hosting a Fedora Core 10 guest as my development system. While I always thought things were somewhat slow on the desktop system, but manageable. Not so on the laptop. So I decided to look into it as I was wasting a lot of time. In particular one observed behavior was the right after starting up ZS cpu usages would go to 100% and stay there for quite a while, even though I was not "touching" anything. In fact it could last as long as 35 minutes.

Investigation first led me down the wrong path. My laptop is limited to 2.5GB memory and the VM I was using only had about 1.2GB. There was, however, no paging going on. I finally discovered that a serious amount of disk IO was happening. Then I discovered that a giant file existed in the php plugin's cache directory (/workspaces/DefaultWorkspace/.metadata/.plugins/org.eclipse.php.core/.cache). It was 74M. There was an accompanying file named "includes.xml" which made me suspicious. I peeked inside the giant file and found references to, and the content of, many files that had nothing to do with the project.

The cause

I wondered why ZS was even looking at these files, and then "the light went on." I use a "lib" directory that contains 3rd party libraries that I use, such as the Zend framework. Here is what that directory looked like:
[dolf@fc10 lib]$ ls -l
total 4
lrwxrwxrwx 1 dolf dolf 29 2009-07-27 15:01 jpgraph -> ../libreleases/jpgraph-2.3.3/
lrwxrwxrwx 1 dolf dolf 31 2009-07-27 15:00 krumo -> ../libreleases/krumo-0.2.1a-ds/
lrwxrwxrwx 1 dolf dolf 29 2009-07-27 14:59 Smarty -> ../libreleases/smarty-2.6.19/
drwxrwxr-x 8 dolf dolf 4096 2009-05-06 10:11 wso2
lrwxrwxrwx 1 dolf dolf 56 2009-07-27 14:58 Zend -> ../libreleases/ZendFramework-1.8.3-minimal/library/Zend/
Well, not quite! This is what it looked like after I solved the problem. Before the directories linked to where not in the "libreleases" directory, but in the "lib" directory. I figured that was fine because I included the "lib" directory on the include path and files would be referred to as "Zend/...".

Sure enough the files would be found, but somehow or other, ZS insists on inspecting every single file that is reachable via the include path. Since I had some 4-5 old versions of Zend Framework hanging around there, as well as some older version of the other stuff, the total amount of files inspected was many times more than necessary. After I "solved" this problem the aforementioned include file went from 74M to about 10MB. The 100% CPU period became much, much shorter.

The overall impact of this was even larger than you might think because a laptop's disk throughput is lower and because I had much less memory than on the desktop (8GB physical, 2GB for the VM), there was less use of a disk cache.

Startup settings

During my search I also encountered some ideas about optimizing the startup settings of Eclipse to make sure it had enough heap memory. I did apply those settings and they seemed to improve things as well. Here is the contents of my current ZendStudio.ini file:
--launcher.XXMaxPermSize 300M
-startup
plugins/org.zend.php.startup_1.0.0.jar
-vmargs
-Dorg.eclipse.swt.browser.XULRunnerPath=/home/dolf/Zend/ZendStudioForEclipse-6.1.1/plugins/org.mozilla.xulrunner.gtk.linux.x86_1.8.1.3-20070404/xulrunner
-Xms375M
-Xmx600M
The ones that are of particular interest here are:
  • XXMaxPermSize: The maximum size of the "permanent" generation on the heap. You may need to "play" with this for your situation.
  • Xms: This is the minimum size of the "regular" heap. Initially set it high and observe Ecelipse's "Heap Status" widget "Preferences>General>Show Heap Status". Wait until you have a stable situation typical of your normal workload. You should set the minimum size to something close to what you see here.
  • Xmx: This is the maximum size of the "regular" heap. Set it somewhat higher than the minimum as determined above.
There are potentially many other Java VM settings you can play with, but I'll leave that to you (I didn't touch them).

Saturday, July 25, 2009

My laptop battery bulged and deformed!

Yesterday everything was fine, tonight I noticed my 17" MacBook Pro was wobbling like a table on even ground. I looked under it and the battery appeared to have come loose. I shut everything down and flipped it over. Well… it was a little worse than that.

With difficulty I removed the battery and noticed something inside had swelled up tremendously. Not good. I remembered a battery recall notice from a while ago, so I went looking. I did check at the time but since my battery was not actually exhbiting any symptoms, the Apple web site told everybody to run Battery Update, which I did.

Coming back to the site now I find that while my computer was covered in the plan, it is now too late. The program closed in May 2009. Browsing around in the forums I find plenty of accounts from users that are in the same boat. Bought their laptop in similar time frame and only now does the problem happen. The original article admits a design/manufacturing issue, but yet no replacement.

I decided I wanted to call Apple to see if I could get a replacement battery anyway. Normally I would have gone to the Palo Alto Apple store, but as it so happens I am in West Yellowstone Montana at the moment. The nearest Apple Store is in Salt Lake City, over 6 hours away! No go, at Apple Care (the only phone number really that one can find to call), is closed. I decided to call my "home" store and see what they can do.

The person in the Palo Alto store keeps insisting that batteries do not normally last that long: Tough, buy a new one. He insists I should go to a Store. The message that the nearest store is 100's of miles away falls on deaf ears the first several times. After insisting about the Apple program about these batteries etc. and that this is a safety related issue, he tries to get a "Genius" to help me. They're all busy. I get told to call Apple Care tomorrow and get case number. With that in hand I should call back to see if, maybe they can ship me a battery.

I am somewhat hopeful about getting a replacement as the messages in some boards seem to indicate that it does happen. In fact, some even got them shipped overnight. Of course that is what I am hoping as I am using this machine every day and waiting for the battery for a week would not be good. More later.

UPDATE: 07/27/09
Talked to AppleCare yesterday (I do not have a contract) and after some hemming and hawing that this is "normal" I managed to convince them that I know it is not. Basically what they said is that it is normal, but if the battery deforms there is a small potential for it to damage other parts of the laptop and told me to bring it in to have it looked at. If, and only if, there is damage would Apple then repair the damage and issue a new battery. I explained that I am nowhere near an authorized repair center (turns out the nearest one is 2hours away in Bozeman, MT) and that I had discovered the problem right after it happened (it seemed to be a sudden event), and could find no visible damage. With that in mind, I asked them to see if they could spare me the trip and send a battery.
It took 10 minutes on hold and then it was approved! Called back today and got a tracking #. It tracks and the battery is supposed to be here tomorrow. Great that Apple does work with customers this way, but too bad that if you don't know how to push the buttons, how to call when it seems they won't listen (because you don't have Apple Care), or if you live near a store you are likely to not get the same service.

UPDATE: 07/28/09
Battery arrived, opened the box, put the old battery in, ripped of the label to reveal a return shipping label, and gave the box back to the Fedex guy who was friendly enough to wait a few minutes for me to do that. Done!

UPDATE: 07/01/11
Same thing happened to my wife's slightly older 13" MacBook. Called them again and had to go through supervisor to end up with customer relations (I spoke with Charles). Again they insist this is normal, and every case is handled without regard for previous cases. He saw the case this post is about in the system, but refused replacement in this case. In both cases the system was out of warranty. He refused to tell me why this case was different from the prior case and refused to send a new battery. It appears that if the battery bulged to the point of doing damage inside the laptop, they will take responsibility. Perhaps the advise to customers should be to leave the battery in until damage has been done! It did not seem to matter that I have been a Macintosh customer since 1984, currently have 4 actively used Macintoshes (2 desktop, 2 laptop) in my house, as well as 2 iPhones and three iPods. They are willing to really tick of a customer over this. From now on my message to every potential laptop buying friend will be that after 1-3 years they will have to spend $130 on a battery because Apple does not stand behind its product in a consistent manner. The replacement battery, consequently will not be bought from Apple.

Tuesday, July 21, 2009

WSF/PHP apparent hang: PHP script exceeded 30 seconds

The problem

So I am using the WSF/PHP web services for a project. All was fine until usage of the web services was stepped up. After a while web service calls started timing out PHP (3o seconds exceeded). Regular web pages were fine, so it had something to do with Web services. I was stumped, but at some point I noticed that the log files produced by the wsf_c extension were just a tiny bit larger than 32MB and my suspicion was raised.

I recreated the situation on a development system by creating a dummy log file just under 32M and then doing some requests. I used top to observe the httpd process being busy for 30 seconds and while it was, attached gdb to it. Some stepping and tracing revealed the cause.

The wsf_c extension starts up early when apache starts and loads the worker processes. It loads mod_pdp, which then causes PHP to load the extension. At this point Apache still runs as root. The extension attempts to write a few log lines, causing the log file to be created, owned by "root" and with permissions "rw-r--r--". Next Apache switches away from root (for security reasons), in my case to user "apache". The running httpd worker processes inherit the open file descriptor and now, effectively, have write permission to this file, so all is fine and logging can continue as needed. I can separately argue the merits of this strange write permission, but it is not relevant here.

Logging continues until the log file exceeds 32MB, which is the built in size limit for the underlying Apache Axis code. The code now copies the log file to "logfile.old" and then closes the log file and attempts to reopen it using "fopen(file, "w+")". This would normally truncate the large log file, as intended. However, it fails to reopen because of a permission issue. The process is "apache" but the file is owned by "root". Logging silently fails and things continue.

So what is the problem? Well, the attempt to log one line of output caused 32MB of data to be copied. The next attempt at a line of logging will do the same because the file was never truncated. If you are logging enough lines per request, the time involved in all that (useless) copying will cause your script to exceed 30 seconds, and bingo: it gets cut off. Of course no logging whatsoever happens.

The solution: part I

The obvious solution is for the WSF/PHP folks to fix the problem (perhaps deferring to the Axis folks). Either delay the log file creation until Apache is running as effective uid "apache", or change ownership of the created log file. If history is any indication, this may take a while to get done.

The solution: part II

The work around I have installed is to run an hourly cron script that inspects the file size. When it gets uncomfortably close to 32MB it restarts the server. The theory is that on restart the rotation attempt happens as well, but now it is done as root and everything should be fine. Not soooo. Unless you force the restart right when there is not enough room to do the initial startup logging, the logging will not cause a rotate and nothing will have changed.

The solution: part III

So finally, I modified my script to not restart the server, but to change ownership of the file. I do this as soon as the file appears to exist, regardless of size. Running this once an hour, or so, will not allow the logfile to reach 32MB before the ownership is changed.

Monday, June 22, 2009

WSF/PHP password callback improvements

So you are working with WSO2's WSF/PHP web services for PHP framework and you need to receive web service calls that contain a username token so you can identify the caller, and apply some password authentication. You read the documentation and find that the WSSecurityToken object will take a callback specification.

Being the good engineer that you are, you write a class, and attempt to have one of its methods called by using "array($theObject, 'mycallback')" as the callback. It is, after all, a standard way of specifying callbacks in PHP. Then you find out that your callback is not being called, but messages in the log files are not helpful. I'll save you the rest of the story and trouble, because it involves digging through the C source code. It turns out that, basically, only a string is accepted for the callback, thus only allowing the identification of a global callback function. Ugggh!

So, I couldn't let this stand. I asked the WSO2 folks to implement proper support, which should not be that hard, but so far there is no action on that topic. So I decided to build a little framework to give me what I wanted. First, let's have a look at how you would define the security options for a simple example:
$securityToken = array(
'callback' => array(
array(
'class' => 'SF_Callback_PasswordCallbackImpl',
'options' => array(
'opt1',
'opt2'
),
'passwordType' => 'Digest'
)
)
);
Notice the "normal" "passwordType" option, but the non-standard "callback" option. The contents of this option is what needs to be converted into a "passwordCallback" that actually calls a method in an object.

To make this all flexible, we will have some code that inspects an object for the interfaces it implements (using reflection), and then configures the appropriate callbacks. First we define a class that will text a set of options, typically specified in an array, and convert it to its "live" form.
interface SF_Callback_ContextProvider
{
/**
* Create a "context" object for a callback.
*
* There is no implied requirement on the object/data created,
* and in fact, it need not be an object, it could be a simple
* type, an array, or an object.
*
* @param mixed $options
* @return mixed Created context object
*/
public function createContext($options);
}
This functionality will be used for all kinds of callbacks, so we then define additional interfaces that specify the particular callbacks (don't worry about the purpose of the above yet, it'll be explained later). Here is one for the password callback function:
interface SF_Callback_Password extends SF_Callback_ContextProvider
{
/**
* Get the password for a specified user
*
* @param string $username Incoming username
* @param mixed $context Additional context data
* @return string The password for the user (for comparison), or null if not found
*/
public function getPassword($username, $context);
}
So with these defined, lets have a look at how you could put together a class that will "transfer" the options so that you can feed them to the constructor of the WSSecurityToken class. The class in question is pretty straightforward, but a little large to present in a single piece, so we'll do it in fragments.

class SF_Callback_OptionConverter
{
private static $_CALLBACK_CONFIG = array(
'SF_Callback_Password' => array(
'getPassword',
'WSF_CallbackFunction1',
SF_Constants::CONFIG_SECURITY_PASSWORD_CALLBACK,
SF_Constants::CONFIG_SECURITY_PASSWORD_CALLBACK_DATA
),
'SF_Callback_ReplayDetection' => array(
'isReplay',
'WSF_CallbackFunction2',
SF_Constants::CONFIG_SECURITY_REPLAY_DETECTION_CALLBACK,
SF_Constants::CONFIG_SECURITY_REPLAY_DETECTION_CALLBACK_DATA
)
);


private function installInterfaceCallback(array &$options, $callbackObject,
$method, $globalCallback,
$callbackOption,
$callbackDataOption = null,
$dataOptions = null) {
}

private function getReflectionClass($callback) {
}

private function installCallbackForInterface(array &$options, $callbackConfig,
$callbackObject, $interfaceName) {
}

public function installCallbacks(array &$options) {
$callbacks = $options['callback'];
foreach ($callbacks as $callbackConfig) {
if (is_array($callbackConfig)) {
$reflectionClass = $this->getReflectionClass(
$callbackConfig['class']);
/**
* Inspect implemented interfaces, and for each one recognized
* install a callback to the specified method.
*/
$callbackObject = null;
$interfaces = $reflectionClass->getInterfaceNames();
foreach ($interfaces as $interfaceName) {
// Create the object once we have a workable interface
if (empty(
$callbackObject)) {
$callbackObject = $reflectionClass->newInstance();
}
$this->installCallbackForInterface(
$options,
$callbackConfig,
$callbackObject,
$interfaceName);
}
}
else {
// Regular callback specification
}
}
}
Let's start with "installCallbacks." You would feed it the array for "securityToken" we have above and it would transform that array. In fact, as you can see, you could have more than one "callback" element because this method iterates over all of them. For each such configuration found, it extracts the name of the implementing class, and uses reflection to find out more about it. In particular, it extracts all interfaces it implements and iterates over those.

Then it gets interesting. The creation of the actual callback object is delayed until we have found at least one interface (it is also done only once, because one object implements all interfaces). So, let's have a look at how a callback for an interface is installed.
private function installCallbackForInterface(array &$options, $callbackConfig,
$callbackObject, $interfaceName) {
if (isset(self::$_CALLBACK_CONFIG[$interfaceName])) {
list($method, $globalCallback, $cbOptionName, $cbDataOptionName) = self::$_CALLBACK_CONFIG[$interfaceName];
$cbConfigOptions = isset(
$callbackConfig['options']) ? $callbackConfig['options'] : array();
$this->installInterfaceCallback($options,
$callbackObject,
$method,
$globalCallback,
$cbOptionName,
$cbDataOptionName,
$cbConfigOptions);
unset($options[SF_Service_W'callback']);
}
}
We inspect our global configuration array to find a configuration for the named interface. If there is one, it will contain the name of the method in the class that should be called, the name of a global function that will be used as the callback installed for WSF/PHP, and the name of the WSF/PHP option under which this callback should be installed. Finally, there is also the name of the WSF/PHP option that can be used to pass additional information to the callback. With that, we can install a callback for one interface:
private function installInterfaceCallback(array &$options, $callbackObject,
$method, $globalCallback,
$callbackOption,
$callbackDataOption = null,
$dataOptions = null) {
$options[$callbackOption] = $globalCallback;
$callbackData = array(
array(
$callbackObject,
$method
)
);
if (!empty($callbackDataOption)) {
$callbackData[] = $callbackObject->createContext(
$dataOptions);
}
$options[$callbackDataOption] = $callbackData;
}
The first thing done here is to add the actual callback option to the array, pointing to the global function. WSF/PHP allows us to pass additional data to this callback, so we go about constructing it here. Only a single value may be passed, and we need more than one, so we'll use an array with at least one element. That is the function of the "createContext" method we ignored earlier. Not only will it put whatever it needs from the "dataOptions" array in a single array, it can also inspect the options and modify them (replace them with objects etc.).

The first element will be the "normal" callback specification that will actually call the method in the object we want. If the configuration did not specify any more options, that would be it, but if it did (as in the example with opt1, and opt2), we add them to the array here and install the array in the configuration as well.

What remains, then, is to show you the global function required to complete this picture.

function WSF_CallbackFunction1() {
return WSF_GenericCallbackFunction(1, func_get_args());
}

function WSF_CallbackFunction2() {
return WSF_GenericCallbackFunction(2, func_get_args());
}
As you can see, the only purpose of each is to call a generic function that needs to know how many of the arguments passed to it are meant for the object's method. The actual number of arguments passed will be one more, as the first one will be the PHP callback specification to the method:
function WSF_GenericCallbackFunction($numArgs, array $args) {
/**
* First several are "real" supplied arguments, and next
* one is the specially coded "data" argument. We agree that
* this will be an array of two elements:
* 1) A callback specification to a "real" callback, meaning it
* can be class method using array specification
* 2) The "data" to be passed on the the actual callback.
*/
@list($realCallback, $data) = $args[$numArgs];
$callbackArgs = array_slice($args, 0, $numArgs);
if (isset($data))
$callbackArgs[] = $data;
return call_user_func_array($realCallback, $callbackArgs);
}
So, the overall chain of events will be that WSF/PHP calls the global callback that we installed (e.g. "WSF_CallbackFunction1"), which will receive the arguments the parameters document with WSF/PHP. The last of these will be the "context" parameter which will have to be "unwrapped" here. Then "WSF_GenericCallbackFunction" will be called. It peels off the last element of the argument array as the "context" and keeps the rest to pass on to the callback (shortening the argument array to remove the "context").

The last element itself is split up into the "real callback" and the additional data that it should receive (opt1, opt2, etc.). We then tack these extra data elements on to the end of the parameter list for the callback, and call it. There you have it.

Oh no, but not quiet!

One little problem. When you try this, you will find your callback does get called, but the "context" parameter never arrives, at least not in version 2.0.0. of WSF/PHP. Inspection of source code reveals this as an oversight/bug. I have passed the patch on the the WSO2 folks, but they are non too speedy, so here is the patch. If you can compile WSF/PHP from source, you can use this patch and be on your way. You'll have to patch the "wsf.c" file:

diff -b wso2-2.2.0/wso2-wsf-php-src-2.0.0/src/wsf.c wso2DS/wso2-wsf-php-src-2.0.0/src/wsf.c
2011a2012,2016
> if(zend_hash_find(ht, WSF_PASSWORD_CALLBACK_ARGS, sizeof(WSF_PASSWORD_CALLBACK_ARGS),
> (void **)&tmp) == SUCCESS)
> {
> add_property_zval(object, WSF_PASSWORD_CALLBACK_ARGS, *tmp);;
> }
Live after the patch makes it into WSF/PHP

All the above will not become obsolete once the patch is in WSF/PHP. While you will be able to directly pass a "real callback," there is still the limitation of only a single "context" argument. It would be easy to modify the code above to no longer use the global callbacks, but leave everything else in place.