The new series of MoneyPress plugins that is coming out in the next month is going to be based on a common foundation. This allows us to maintain consistency, share new features across the product line, and provide an improved quality product that gets out to the consumer.
However, during the migration to this new shared platform we uncovered some problem areas deep within the bowels of WordPress. Yes, even with the recently released 3.0 version. However we don’t blame this on WordPress. Far from it. WordPress is a well engineered application, it’s only fault is being tied into archaic versions of PHP… which means anything prior to PHP 5.3 when namespaces were finally introduced. There is a reason many languages have had namespaces for years, but that is a discussion for another post.
One of the more nagging problems was an issue with adding the settings pages to the admin panel menus. As soon as we activated a 2nd (or 3rd) plugin using the same base classes, the program broke. Only the latest plugin to be loaded would show up. We ruled out basic syntax and logic errors fairly quickly.
The problem, it turns out, has to do with how WordPress builds the internal names for all of these objects we are creating based on the same class. It was a lot of debugging of our code as well as the WordPress code that resulted in a simple solution.
Assumptions – We All Know What They Say About That!
Secondly, WordPress uses the *class name*, not the *object* itself when passing an array as the function to the add_options_page or add_action functions. This is a very important distinction. The way the docs are written one would ASSUME WordPress is using the object to fire off the functions.
Direct quote from the WordPress Docs…
The function must be referenced in one of two ways:
- 1. if the function is a member of a class within the plugin it should be referenced as array( $this, ‘function_name’ )
- 2. in all other cases, using the function name itself is sufficient
Since $this would be the instantiated class, I jumped to the same conclusion as I believe Chris (and/or Eric) would have… “you pass the object, therefore it must call that object’s function_name function”. Not true.
Why The Problem Occurs
The code, buried in the very end of wp_includes/plugin.php does something LIKE THIS (I’ve put in the debugging statement versus actual code, it gets the point across in 10 fewer lines) on versions BEFORE 5.2 :
echo 'returning ' . $tag .' ID for ' . get_class($function) . ' as ' . $obj_idx . '<br/>';
That means they are getting the CLASS NAME for the object, attaching the 2nd string which is the function name and using THAT to come up with a unique key. Guess what happens if the class name + function name are already IN the list? That’s right, NOTHING. Since all of our plugins are now based on the WPCSL-GENERIC class name, only one menu item is added and only one render-the-settings page is put into the queue.
In case you’re wondering they do a function_exists(‘spl_object_hash’) to test for 5.2+ functionality in PHP. If that function exists they do this instead, which yields similar unwanted results:
return spl_object_hash($function) . $function;
As a side note – I like how they do their 5.2 “upgrades on the fly” by checking for a function from 5.2, using that if they can, otherwise use their own code.
All told, this is yet ANOTHER great argument for PHP based applications to all upgrade to 5.3 and start using namespaces. Also, IMO this is a bad way to manage this. Every plugin on every site must have unique class names or this will fail. As we found out when doing the “right thing” and basing all of our WordPress plugins off the same base classes.
So after all of this, there is a somewhat simple fix. Set the wp_filter_id propery on your class.
In our class we simply need to set the property wp_filter_id = a unique int. The only thing we need to make sure of, since we are now our own ID number managers, is to ensure EVERY PLUGIN we create has a unique wp_filter_id property. My shortlist for wp_filter_ids (which we need to record in our internal docs):
- MP : CafePress Edition = 1
- MP: Commission Junction Edition = 2
- MP: eBay Edition = 3
- MP: BuyAt Master Edition = 4
- MP: Ticketmaster Edition = 5
- MP: NY Times Store Edition = 6
Not a GREAT fix because it bypasses the WordPress ID generator for the filter system. It is very much like turning off auto-index on a database primary key for a FEW records. Luckily the IDs in WordPress are compound keys made up of class name + function name + autoID. Since our class name + function name is somewhat long & complex there is little chance that setting a manual ID will cause problems.
The reason this works? In that deep dark function called _wp_filter_build_unique_id the guys on the WordPress development team left an “out”. If your class has wp_filter_id set as a property it skips their auto-generation of the ID, assuming you know what the heck you’re doing. This means it doesn’t find the class name + function name already in the table and “skip it” because it thinks it’s doing the same thing twice. You told it use ID # X so it will do that… and thus create a manually generated unique ID for each plugin even though they share the same base class. When it comes time to render the page both new filters are on the render stack & will get popped off, drawing each menu item where it belongs.
What a way to get a crash course on WP3.0 internals. Now on to launching some new products…