I am building a simple WordPress plugin and I am looking to add unit tests using PHPUnit, I have the following class code:
<?php
namespace App;
class MyPlugin {
public function __construct()
{
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
add_action('admin_menu', [$this, 'createAdminMenu']);
add_action('admin_init', [$this, 'settings']);
}
public function enqueueAdminScripts()
{
// Code here
}
public function createAdminMenu()
{
// Code here
}
public function settings()
{
// Code here
}
}
And this is my test file:
<?php
use App\MyPlugin;
use PHPUnit\Framework\TestCase;
class MyPlugin extends TestCase {
protected $myPlugin;
protected function setUp(): void
{
$this->myPlugin = new MyPlugin();
}
public function testRegister()
{
$this->assertInstanceOf('\App\MyPlugin', $this->myPlugin);
}
}
However I am always getting the error Error: Call to undefined function App\add_action()
What is the best way of mocking the add_action calls purely within PHPUnit without having to rely on a third party solution?
Any help with this would be greatly appreciated.
Below is a very simple mock of the hook system. It doesn't take into account priorities, and it is more modernized for expected usage so it might not support some previous edge cases.
You'll notice that add_action actually calls the exact same code as add_filter, and that's what core actually does.
The gist is that there's a global array. You could store this in your base class if you wanted, too. WordPress backs their stuff with objects, too, but I'm simplifying things here.
global $wp_test_hooks;
if(!is_array($wp_test_hooks)) {
$wp_test_hooks = [];
}
if(!function_exists('add_filter')) {
function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
global $wp_test_hooks;
$wp_test_hooks[$hook_name][] = [
$callback,
$priority,
$accepted_args,
];
return true;
}
}
if(!function_exists('add_action')) {
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) {
return add_filter( $hook_name, $callback, $priority, $accepted_args );
}
}
if(!function_exists('apply_filters')){
function apply_filters( $hook_name, ...$values ) {
$value = array_shift($values);
global $wp_test_hooks;
if(array_key_exists($hook_name, $wp_test_hooks)){
foreach($wp_test_hooks[$hook_name] as $parts){
list($callback, $priority, $accepted_args) = $parts;
$value = $callback($value, ...$values);
}
}
return $value;
}
}
if(!function_exists('do_action')){
function do_action( $hook_name, ...$arg ) {
global $wp_test_hooks;
if(array_key_exists($hook_name, $wp_test_hooks)){
foreach($wp_test_hooks[$hook_name] as $parts){
list($callback, $priority, $accepted_args) = $parts;
$callback(...$arg);
}
}
}
}
You can see it in action by using normal WordPress code:
add_action('name1', static function(){echo 'In the Action';});
add_filter('name2', static function($originalValue){return 'In the Filter';});
do_action('name1', 'test', 'stuff');
echo PHP_EOL;
echo apply_filters('name2', 'test', 'stuff');
Demo here: https://3v4l.org/DAVl0
All that said, if you don't manually call do_action or apply_filters in your code, don't both with the bulk of this. You can use the functions, just leave their bodies empty. More often than not, it is WordPress or a plugin that is invoking those methods, and if you aren't invoking WordPress or calling those functions, there's no need for logic or even the global array. What you probably want is:
if(!function_exists('add_filter')) {
function add_filter( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
}
if(!function_exists('add_action')) {
function add_action( $hook_name, $callback, $priority = 10, $accepted_args = 1 ) { }
}
if(!function_exists('apply_filters')){
function apply_filters( $hook_name, ...$values ) { }
}
if(!function_exists('do_action')){
function do_action( $hook_name, ...$arg ) { }
}
I'd say this depends a lot what you'd actually want to test (and therefore call).
Reading your question you would like to put Wordpress itself out of the equation to essentially test your code.
That is testing a plugin not as an integration test.
Now Wordpress does not make it exactly easy to test but you could try to get as far as possible. I'd say the issue you describe in the question lays in the constructor:
public function __construct()
{
add_action('admin_enqueue_scripts', [$this, 'enqueueAdminScripts']);
add_action('admin_menu', [$this, 'createAdminMenu']);
add_action('admin_init', [$this, 'settings']);
}
This constructor only works within Wordpress. But now you want to test it without. Well, easy:
public function __construct()
{
}
Problem sovled. Now you'd say, this is overly clever, right? The plugin needs to be initialized. True:
public static function setup(): MyPlugin
{
$plugin = new self();
add_action('admin_enqueue_scripts', [$plugin, 'enqueueAdminScripts']);
add_action('admin_menu', [$plugin, 'createAdminMenu']);
add_action('admin_init', [$plugin, 'settings']);
return $plugin;
}
With this, you still have one central way to setup your plugin while you're able in your unit-tests to test calling the concrete functions. The new operator (in your test) and the way you've written the constructor does not stand in your way any longer and the code became testable.
If those as well depend on Wordpress functions, you'll run into the same problem thought. However the functionality you actually may want to test here might not entirely, so extracting the code that does not into methods of its own, allow actual unit-tests of those independent methods (those that have no side-effects on the global scope which Wordpress uses).
And I'd say your nose is good here: At the end of the day you want as many of these pure methods (without side-effects) and less those that depend on global state and Wordpress as a dependency.
You could do other tests (e.g. integrating with Wordpress) with the Wordpress base test case class to not reinvent the wheel. To only do that for certain methods, group your tests in different directories, e.g. test/unit and test/wordpress.
Then you can decide what kind of test you want to write.
Related
I'm learning to code Wordpress Multisite the OOP way, and since I'm pretty new to OOP now I am in a situation that I can't seem to solve on my own.
Specifically, I'm creating some classes to create admin pages (both at the network and subsite level) with an OOP approach. Here's my simplified code:
class AdminPage {
public function __construct( $args ) {
add_action( 'admin_menu', array( $this, 'add_admin_page' ) );
}
public function add_admin_page() {
add_menu_page( // arguments );
}
}
class AdminNetworkPage extends AdminPage {
public function __construct( $args ) {
add_action( 'network_admin_menu', array( $this, 'add_admin_page' ) );
}
}
The code works, but as you can see I have to extend the AdminPage class with the sole purpose of changing the hook in the constructor ( I need admin_network_menu for admin pages at network level instead of admin_menu).
Is there a better way to do this? A way to have one class, put both hooks in the constructor of that class and then selectively call either one or the other?
Unfortunately, creating a new instance of the class ($page = new AdminPage) and then calling a method on it (e.g. $page->add_admin_page() ) won't work in this case, because then I get an error saying add_menu_page is undefined... It all has to happen in the constructor.
You could do something like this:
class AdminPage {
public function __construct( $args, $networkPage = false ) {
if($networkPage) {
add_action( 'network_admin_menu', array( $this, 'add_admin_page' ) );
} else {
add_action( 'admin_menu', array( $this, 'add_admin_page' ) );
}
}
public function add_admin_page() {
add_menu_page( // arguments );
}
}
And then pass a second parameter to the class to change which action you add, e.g.
$page = new AdminPage($args);
$networkPage = new AdminPage($args, true);
Whether or not this is better than just extending the class is debatable; I personally think there's nothing wrong with your initial setup, the point of OOP isn't to create as few classes as possible - if your code make more sense as two separate classes (with one extending the other) then there's nothing wrong with that.
I am trying to pass an argument (say $arg) to the second parameter/callback of add_action() using do_action() in Wordpress.
do_action( 'my_hook', $arg);
the 'my_hook' in do_action() will refer to the below add_action()
class My_Class {
public function __construct () {
add_action( 'my_hook', array( $this, 'my_method ($arg)' ) );
}
public function my_method ($arg) {
some_function ($arg);
// some code here....
}
}
Note: I am not trying this in fuctions.php. Trying to modify core files. I know modifying core files is not recommended. But, I want to explore the horizons (I will return back to the home though)
Please provide a way to pass the argument into my_method(). Thanks in advance.
Hm I think this should work if you take out the ($arg) from the add_action call.
class My_Class {
public function __construct () {
add_action( 'my_hook', array( $this, 'my_method' ) );
}
public function my_method ($arg) {
some_function ($arg);
// some code here....
}
}
// make sure you are creating a new object so __construct() is called
$instance = new My_Class();
do_action( 'my_hook', $arg );
If you look at the documentation, add_action lets you specify the number of arguments an action accepts. It defaults to 1.
https://developer.wordpress.org/reference/functions/add_action/#more-information
I have the following code, perform a global function within a class to fill the functions of wordpress, the problem is that the only way that I could get a variable public class is as follows
class Core {
public $notice;
function __construct(){
$this->core_function();
}
function core_function(){
global $globalvar;
$globalvar = $this;
function notice_global(){
global $globalvar;
return $globalvar->notice;
}
}
function set_notice(){
$this->notice = array('Warning');
}
}
$GP = new Core();
$GP->set_notice();
var_dump(notice_global());
Any other ideas or suggestions, this code is correct or not?
As you said in the comments, you need global function due to wordpress hook method (for a plugin, I suppose).
This is not necessary: there is a way to pass an object method (not a whole object) to wordpress.
You can try in this way:
class Core {
public $notice;
function get_notice()
{ return $this->notice; }
function set_notice()
{ $this->notice = array('Warning'); }
}
$GP = new Core();
$GP->set_notice();
add_action( 'save_post', array( $GP, 'get_notice' ) );
Or - for a better flexibility - in this way:
class Core {
public $notice;
function get_notice()
{ return $this->notice; }
function set_notice()
{ $this->notice = array('Warning'); }
function add_wp_action( $hook, $method )
{ add_action( $hook, array( $this, $method ) ); }
}
$GP = new Core();
$GP->set_notice();
$GP->add_wp_action( 'save_post', 'get_notice' );
By this way, you can directly set all your wp hooks in the class and call they directly with an object method, without using globals variables or function tricks.
I'm not sure if I'm understanding you right, but notice_global can be moved out of that class.
Globals have scope outside of classes
There is no need for these functions. You've defined $notice as public property. You can access it like this: $GP->notice;
You might also want to read documentation on visibility of methods and properties.
I need some help, I've been through so many of these "similar" issues on stackoverflow and all over the web but nothing seems to be helping.
I'm new to wordpress and started creating a plugin using classes. when I run the plugin it throws the error "Call to undefined function add_menu_page()". I've checked the support docs on wordpress.org but that doesn't help either (https://codex.wordpress.org/Adding_Administration_Menus#Inserting_the_Pages)
heres my code below please can you tell me what I'm doing wrong:
class SetupAdminMenu {
public static function add_menu($menu)
{
add_action('admin_menu', self::load_menu($menu) );
//I've used it according to the docs as well
//add_action('admin_menu', array(self, self::load_menu($menu) );
}
public static function load_menu($menu)
{
$page = add_menu_page( $menu['page_title'], $menu['menu_title'], $menu['capability'], $menu['menu_slug'],
array(self,self::load_view($menu['function'])), $menu['icon_url'], $menu['position'] );
self::load_page($page);
}
protected static function load_view($view)
{
if ( !current_user_can( 'activate_plugins' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
}
//The actual page to display
include_once($view);
}
protected static function load_page($page)
{
add_action( 'admin_print_styles-' . $page, 'admin_styles' );
}
}
Thanks peeps
Couple of things going on here. First to just clarify, add_menu_page is to add an administration menu page for managing settings, etc.
Your add action should be as follows:
public static function add_menu($menu)
{
add_action('admin_menu', array('SetupAdminMenu', 'load_menu'));
}
The way WordPress handles actions requires you to format your add_action as follows for static class methods:
add_action('admin_menu', array('ClassName', 'methodname'), $priority, $numVars);
There are two optional variables:
$priority = integer, If there are multiple actions to a hook, the priority will determine the order of load. Default is 10. Use 99 if you want to react to any earlier actions, use 1 if you want to define things before further actions
$numVars = interger, if the method or function requires more then one variable, put the number here. WordPress core hooks don't require this to be defined and will pass variables according to the documentation.
Next, the class method with "add_menu_page" is pretty off. To my knowledge admin_menu hook does not return any variables by default and even if it did, why do you want the existing menu when trying to add new items? You pass those variables yourself:
public static function load_menu()
{
$page_title = 'My New Page';
$menu_title = 'New Page';
$capability= 'manage_options'; //An administrator only capability
$menu_slug = 'my-new-page';
$callback = array('SetupAdminMenu', 'load_view');
$menu_icon = ''; //use default
$menu_position = 81 //After settings
add_menu_page( $page_title, $menu_title, $capability, $menu_slug,
$callback, $menu_icon , $menu_position);
}
Your self::load_page($page); is redundant. The action you use in that function does nothing. The callback is what is displayed when the menu item is clicked. No reason to make that protected, it would likely fail anyway because of the way class methods are called from hooks within WordPress.
public static function load_view() // Removed $view because you need to define this yourself.
{
if ( !current_user_can( 'activate_plugins' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
}
//The actual page to display
//include_once('/path/to/your/template');
//which should have php, html for what you want to have appear.
//To test that the rest of the code is working... just put:
echo 'Hello World!';
}
This link has a list of a bunch of "how tos" on WP Plugin development.
alright, so I've done some more reading and playing and below is my answer, might help some.
class SetupAdminMenu {
private static $menu = array();
public static function add_menu()
{
add_action('admin_menu', array('SetupAdminMenu', 'load_menu') );
}
public static function add_sub_menu()
{
add_action('admin_menu', array('SetupAdminMenu', 'load_sub_menu') );
}
the method below is what I was missing initially, because you cannot pass an argument from the call back in add_action()
public static function setupPage($params)
{
self::$menu = $params;
}
public static function load_menu()
{
$page = add_menu_page( self::$menu['page_title'], self::$menu['menu_title'], self::$menu['capability'],
self::$menu['menu_slug'], array('SetupAdminMenu', 'load_view'),
self::$menu['icon_url'], self::$menu['position'] );
self::load_page($page);
}
protected static function load_sub_menu()
{
$page = add_submenu_page( self::$menu['parent_slug'], self::$menu['page_title'], self::$menu['menu_title'],
self::$menu['capability'], self::$menu['menu_slug'], 'load_view');
self::load_page($page);
}
public static function load_view()
{
if ( !current_user_can( 'activate_plugins' ) ) {
wp_die( __( 'You do not have sufficient permissions to access this page.' ) );
}
//The actual page to display
include_once(self::$menu['page']);
}
protected static function load_page($page)
{
add_action( 'admin_print_styles-' . $page, array('SetupAdminMenu', 'admin_styles') );
}
public static function admin_styles()
{
wp_register_style( 'admin_styles', plugins_url('css/admin.css', __FILE__) );
wp_enqueue_style( 'admin_styles' );
}
}
because add_action() is used so many times I've removed add_menu() and add_sub_menu() from this class and added the method below to may base class to be called with the necessary parameters for any actions
public static function addAction($action, $callback)
{
add_action($action, $callback);
}
might not be 100% the best way to do it yet but the above currently works.
I am experimenting with PHP+WP the very first time. I intend to use WP plugin hooks. As a C++ programmer I also intend to put all my code into classes. Currently I am kind of stuck with the following snippet that should install a WP plugin hook:
class SettingsHandler
{
public function __construct()
{
add_filter('plugin_action_links', array($this, 'AddSettingsLink'), 10, 2);
}
private function AddSettingsLink($links, $file)
{
if ($file==plugin_basename(__FILE__))
{
$settings_link = 'Settings';
array_unshift($links, $settings_link);
}
return $links;
}
}
$settingsHandler = new SettingsHandler();
This gives me an error message:
Warning: call_user_func_array() expects parameter 1 to be a valid callback, cannot access private method SettingsHandler::AddSettingsLink() in E:\xampp\apps\wordpress\htdocs\wp-includes\plugin.php on line 199
When I switch the callback to public, the error is gone. It seems I can not use a private method as a callback in PHP/WP. This would be very bad because it reveals a lot of callback methods that should not be accessed by anyone else directly. Can I make suchs callbacks private?
I also found the following snippet which runs fine:
class a
{
public function __construct()
{
$str = " test test ";
$result = preg_replace_callback('/test/', array($this, 'callback'), $str);
echo $result;
}
private function callback($m)
{
return 'replaced';
}
}
$a = new a();
Why does the second snippet work while the first fails? Where is the difference?
The second version works because preg_match_all is called from within the class scope and will execute the callback immediately.
But the add_filter function only adds the callback to the global $wp_filter array. The callbacks in that array are then called at a later stage from outside the class you defined the method in. Consequently, visibility rules apply making the callback inaccessible.
You can¹ get around this by wrapping the call to the method into an anonymous function, e.g.
public function __construct()
{
add_filter(
'plugin_action_links',
function($links, $file) {
return $this->AddSettingsLink($links, $file);
},
10,
2
);
}
However, this requires at least PHP 5.4 (see changelog in Manual) due to $this being unavailable in the anonymous function before that version.
Another option would be to have the SettingsHandler implement __invoke to turn it into a Functor, e.g. you add
public function __invoke($links, $file)
{
return $this->AddSettingsLink($links, $file);
}
and change the ctor code to
add_filter('plugin_action_links', $this, 10, 2);
Since the class implements __invoke as a public entry point, the instance can be used as callback. This way, you can keep the private stuff private.
On a side note, I'd move the code adding the filter outside the class. Having it in the ctor makes the class less testable (yeah, I know no one tests WP plugins, but still). Instead, put it into your plugin file and then include everything else required from there, e.g.
// your-plugin.php
include 'SettingsHandler.php';
add_filter('plugin_action_links', new SettingsHandler, 10, 2);
But that's up to you.
¹Note: apparently this doesn't work in Wordpress because it will try to serialize the closure somewhere along the way. In general, this a working pattern to provide private methods as callbacks.