Elegant pass by reference?

I have built a plugin system, which is modeled after how Wordpress does theirs. It's fairly simple, I use call_user_func() to call functions that I am keeping in an array (there are priorities, and a few other things that go on, but this is the part that is relevant to this question).

One of the things I am trying to do is add information to an array that is kept in a class.

class foo {
    $grammar = array()

    function bar() {
       $this->grammar = runAction('load-grammar',$this->grammar);
    }
}

Open in new window


Now, this would run all the plugins that are hooked to 'load-grammar'.

function runActions($requested_hook,$args=false) {
  global $plugins;
  foreach($plugins as $hook => $pluginArray) {
    if($requested_hook == $hook) {
      foreach($pluginArray as $priority => $functionArray){
        foreach($functionArray as $signature => $callback) {
          if($args) {
            return call_user_func($callback,$args);
          } else {
            return call_user_func($callback);
          }
        }
      }
    }
  }
}

Open in new window


Here's the problem, my structure works great ... for the first plugin. But, since I am returning a value, it is only exercising the first plugin. It quits after that.

SO, my immediate thought was: "OK, I'll just create another function that passes the variable by reference." But, apparently, PHP doesn't pass by reference in a manner that allows me to directly modify the array without having to return it. They aren't real pointers.

So, how do I loop through all the functions, which will modify that array, and not have it stop after the first one?
LVL 32
DrDamnitAsked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Ray PaseurCommented:
Not sure I understand the question, but variables and references are one of the "interesting" points in the history of the PHP language.  Please see if this helps...
http://www.experts-exchange.com/Web_Development/Web_Languages-Standards/PHP/A_12310-PHP-Variables-and-References.html
Julian HansenCommented:
Need to clarify
function bar() {
   $this->grammar = runAction('load-grammar',$this->grammar);
}

Open in new window

runAction (singular)
function runActions($requested_hook,$args=false) {
  global $plugins; // OUCH!!!!
  foreach($plugins as $hook => $pluginArray) {
    if($requested_hook == $hook) {
    }
  }
}

Open in new window

runActions (Plural). Was your code supposed to be like that or is there a typo the class is meant to run the runActions

But, since I am returning a value, it is only exercising the first plugin. It quits after that.
This is not clear.
If I look at your code - after you have looped down to the third foreach you are doing an if / else both with a return - so it seems to me the code will bug out the minute a hook is matched - irrespective of what happens next (unless the functionarray var is empty).

So what is it you are actually asking?
Ray PaseurCommented:
Upon rereading this with coffee, there may be some other ideas that could be useful.  My "code smell" antennae twitch when I see the global keyword, so I'll try to work around that idea.

We are not limited to passing and returning simple variables; we can pass and return arrays or objects.  An object can return itself with something like return $this;  Such a design opens up a world of new language and data structures that can be powerfully expressive.  One of the deficiencies of PHP is the positional nature of function arguments.  But we do not have to live with that deficiency; we can use named arguments if we pass an argument object instead of a list of individual arguments.  A type-hint (type declaration) can help us keep track of the argument objects.

Here is a way to structure a collection of plug-ins that might help mitigate the issue with the return values.  Each of the plug-ins keeps its own result and also returns itself, thereby making the result available.
http://iconoun.com/demo/temp_drdammit.php
<?php // demo/temp_drdammit.php

/**
 * http://www.experts-exchange.com/questions/28711556/Elegant-pass-by-reference.html
 */
error_reporting(E_ALL);
echo '<pre>';


// A RUNNABLE PLUGIN OBJECT
Class Plugin
{
    public function __construct($callback, $arguments)
    {
        $this->triggered = NULL;
        $this->callback  = $callback;
        $this->arguments = $arguments;
    }
    public function trigger()
    {
        $this->triggered = TRUE;
        $this->result    = call_user_func($this->callback, $this->arguments);
        return $this;
    }
}


// A CLASS DEFINITION FOR TYPE-HINT TO THE echoData() FUNCTION
Class EchoDataArguments
{
    public function __construct($thing)
    {
        $this->data = $thing;
    }
}


// A FUNCTION THAT USES A TYPE-DECLARATION FOR AN ARGUMENT OBJECT
function echodata(EchoDataArguments $arguments)
{
    $response = 'Data: ' . $arguments->data;
    return $response;
}


// A FUNCTION THAT RUNS ALL OF THE PLUG-INS
function run_all_plugins($plugins_array, $method='Trigger')
{
    foreach ($plugins_array as $obj)
    {
        $obj->$method();
    }
}


// CREATE ARGUMENT OBJECTS
$zero = new EchoDataArguments('Zero');
$one  = new EchoDataArguments('One');
$two  = new EchoDataArguments('Two');


// CONSTRUCT AN ARRAY OF PLUGIN OBJECTS
$arr =
[ new Plugin('echodata', $zero)
, new Plugin('echodata', $one)
, new Plugin('echodata', $two)
]
;
// SAVE A DUPLICATE COPY OF THE PLUG-IN ARRAY HERE
$alt =
[ new Plugin('echodata', $zero)
, new Plugin('echodata', $one)
, new Plugin('echodata', $two)
]
;


// SHOW THE ORIGINAL ARRAY OF PLUGINS WITHOUT THE result PROPERTIES
echo PHP_EOL . "ORIGINAL ARRAY OF PLUGINS: " . PHP_EOL;
var_dump($arr);


// RUN ALL OF THE PLUGINS
// THIS WORKS BECAUSE EACH OBJECT ACTS UPON ITSELF IN trigger() METHOD
run_all_plugins($arr);

// SHOW THE TRIGGERED PLUGINS CONTAINING THE result PROPERTY
echo PHP_EOL . "AFTER CALLING run_all_plugins(): " . PHP_EOL;
var_dump($arr);


// RUN SELECTED PLUGIN(S)
// THIS WORKS BECAUSE EACH OBJECT RETURNS ITSELF IN trigger() METHOD
foreach ($alt as $key => $obj)
{
    if ($key == 1) $alt[$key] = $obj->trigger();
}

// SHOW THE RETURN VALUES OF THE TRIGGERED PLUGINS CONTAINING THE result PROPERTY
echo PHP_EOL . "AFTER MANUALLY TRIGGERING PLUG-IN: " . PHP_EOL;
var_dump($alt);

Open in new window

Learn Ruby Fundamentals

This course will introduce you to Ruby, as well as teach you about classes, methods, variables, data structures, loops, enumerable methods, and finishing touches.

DrDamnitAuthor Commented:
@Julian:

I see that giving small snippets is obscuring the question, so here's the actual code for the plugin:
plugins.php

When I say runActions (plural) I mean it. There are many actions that can be run for a given hook, and those actions can be loaded / run in a specified order.

Here is what plugins look like when they are loaded:

Array
(
    [cli-init] => Array
        (
            [50] => Array
                (
                    [5d67a4409b4d37f3805b2e22cfb92dcd] => CLI\Userutil\declareMyself
                    [7c91246a402a4174e9d485651cbbbc04] => LibLoad\declareMyself
                    [ca7df20e5c301868b2e9fac99808ae4b] => CLI\Defaults\declareMyself
                )

        )

    [lib-loader] => Array
        (
            [50] => Array
                (
                    [c4fc3019743fce68aaa8f86e135b2ab4] => LibLoad\findPHPMailerAutoloader
                )

        )

    [cli-load-grammar] => Array
        (
            [0] => Array
                (
                    [18d54950323b23cb91022e4d98797a08] => CLI\Defaults\loadDefaultGrammar
                )

        )

)]

Open in new window


You can see the hooks are an array key, and the value for that element is an array that contains the plugins / call backs, which have been sorted in order (so you can control which load first).

@Ray:

Globals make me cringe. It is not part of the permanent plan, but was a quick fix to a proof of concept. The entire system will, eventually, be encased in a class where this plugins array will be a property of that system. So, the code you have here that classifies the plugin is quite attractive. I had actually been considering this since I wanted convert this to a class. (I was actually struggling with "do I turn this into a class or keep is super simple as procedural code".
DrDamnitAuthor Commented:
In re-reading my last post, I realized I am not doing a good job communicating this at all.

So, here's a three-minute screencast of the issue and the relevant code:

https://www.youtube.com/watch?v=xpJlLGvSegc
Julian HansenCommented:
Obviously the return will break the foreach as you state in the video.

Why not simply push the return values into an array and return the array?
$results = array();
...
foreach(...)
{
    if (args) {
        results[] = call_user_func($callback,$args);
    }
    else {
        results[] = call_user_func($callback);
    }
}
...
return $results;

Open in new window

Seems overly simplistic to me but I might be missing something.
DrDamnitAuthor Commented:
Because this routine runs for all plugins, even those which do not return a value, and may need return a non-array value (single string, for example)
Ray PaseurCommented:
I haven't read all of the other parts or watched the video, but this jumps out at me.
...and may need return a non-array value (single string, for example)
Why not return an array 100% of the time?  It will simplify your code if the assumption is that the result is an array.  Here's why I recommend that...

1. Monday: Client says, "I'm sure we will only need one return value."
2. Friday: Client says, "Turns out we need two return values."

If the assumption is an array, you can use a foreach() iterator.   Foreach() works correctly with zero, one, or infinity data elements in the array.  But if the assumption is a string, you have spent the week creating code that now needs to be changed.

The same idea extends to passing and returning request and response objects.  The simplicity of a string variable comes at a cost, which is the inflexibility of a string variable.
Julian HansenCommented:
even those which do not return a value, and may need return a non-array value (single string, for example)
Still confused - if you return out of a foreach - you break the loop.

So either you want the loop to run to natural termination - or you don't - from your previous posts it appears you want the loop to run.

So now you have a loop calling N hooked functions - some of which return values some of which don't - what was your thinking on how to process the return - I can think of only 2 ways

1. You do the complete return handling in the loop i.e.
   - call hook
   - get return,
   - process return
   - discard return
   - loop to next hook
2. You buffer the returned values and batch process them after the fact.

My last post assumed #2 - the array simply saves whatever was returned - you could possibly further enhance it by making the key into the array the hook name.
Something like
$results[$hook][$signature] = call_user_func($callback,$args);

Open in new window

Not sure what you need to do with the return so not going to propose a processing method at this point - but those are the options as I see them
DrDamnitAuthor Commented:
I have taken what you both have suggested, and implemented some structural changes.

But, still working to think my way out of this corner... and get rid of the global statement.

I have encapsulated the plugin as a class. I'm using interfaces to make sure they are consistent, and right now the only required method is trigger(). (vis-a-vis, @Ray's comments).

When the application loads, the first thing it includes with a require() statement is application_top.php. The first 300 lines consist of a series of utility functions, the spl autoloader for classes, and the interfaces.php file, which defines the interface(s).

Now, I am using a class that will manage all the plugins (PluginEngine). The intent here is to load the plugins into an array property of the class (PluginEngine::plugins), and have them execute from here, always returning a value (or at least staging it in a plugin property so I can get to it later) vis-a-vis @Julian's comments.

I am not getting this error, which I shouldn't be getting (in my clearly erroneous opinion) because I should be working within the same scope.

Error
It refers to this line (highlighted):
The class
But $PE is instantiated in application_top.php:
Instantiated
How are these not in the same scope?
Julian HansenCommented:
How are these not in the same scope?
It is possible they are in the same scope but between instantiation and use it is getting stomped on.

The trick is to put in regular var_dump / print_r statements between instantiation and line 54 and see where it suddenly changes.
DrDamnitAuthor Commented:
@Julian:

It said it didn't exist. So, I went back and looked at the code, then realized var_dump($this) should give me the $PE because I am including these plugins from within the PluginEngine class.

That worked.

Ok, back to the original main question we've been working on... I'll report back once I get you and Ray's other comments fully implemented.
DrDamnitAuthor Commented:
Out of frustration, I am starting to think I have hit a limit of the PHP langauge. But that can't be the case.

Simply put, I need a class ("B"), which is a property of another class ("A") to be able to access (and set) properties of class "A". But, I think variable scope generally prohibits this.

Normally, class "A" can reach into class "B" and do what it wants. A can pass arguments to B, and have full access to any properties and methods it needs (echo $A->B->name, for example). But, because of variable scope, "B" cannot reach "up" into class "A". The only way to do this, is to return variables from a method.

But this fails too.

I have created a class hook, which stores the hook name, callback, arguments (which are useless now, more on that later) and the priority.

It is executed here:
public function trigger($requested_hook)
    {
        $return = array();
        foreach($this->hooks as $hook) {
            $thisHook = $hook;
            print_r($thisHook);
            $result    = call_user_func(array($this,$hook['callback']));
            array_merge($result,$return);
        }
        return $return;
    }

Open in new window


The callback will return an array like this:
Array
(
    [set] => Array
        (
            [verbosity] => 
            [debug] => Array
                (
                    [on] => 
                    [off] => 
                )

        )

    [show] => Array
        (
            [debug] => Array
                (
                    [environment] => Array
                        (
                            [dump] => Array
                                (
                                    [grammar] => 
                                )

                        )

                )

            [verbosity] => 
        )

    [plugins] => Array
        (
            [0] => show
        )

)

Open in new window


But, it's obliterated by the array_merge(), and becomes Array(NULL). Had that worked, I could have passed each return upstream to the calling function until I get to the top, and then handled it from there, but I don't see another way to pass arrays back through all the functions and get something useful at the top.

What am I missing here?
DrDamnitAuthor Commented:
Frustration is a dangerous thing. I was not using array_merge properly. I had its syntax confused with some of the other array functions, which do not return values.

Pressing on....

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
Julian HansenCommented:
Do you mean something like this. B is passed to A and is assigned to a property of A. A method setParent is called on B passing $this as a parameter.
B now has access to public methods and properties in A.

class B
{
    protected $parent;

    public function setParent($parent)
    {
         $this->parent = $parent;
    }
}

class A
{
    public $b;

    public function __construct($b)
    {
        $this->$b = $b;
        $this->$b->setParent($this);
    }
}

Open in new window

DrDamnitAuthor Commented:
It is nearly impossible to divide points up properly for you two. So, here is the play by play:

Julian helped me to think through passing information back and forth with arrays, and you stayed very engaged. That's really appreciated since I was really needing someone to point out my obvious mistakes.

Ray, you helped me think through executing all the plugins. In addition, after reading some of your posts, I found articles you had written on the subject, and those were helpful as well.

While I can't remember which one, specifically, gave me a hint to the correct direction to go, I really liked this one, and I want you to get some used in solution points, so here we go:
http://www.experts-exchange.com/articles/12310/PHP-Variables-and-References.html
and
http://www.experts-exchange.com/articles/12293/AntiPHPatterns-and-AntiPHPractices.html
DrDamnitAuthor Commented:
@Julian:

I saw your latest comment after I closed the question. Great comment, but I've already solved it with your previous ideas about passing an array back up the chain. Thank you, though.
Julian HansenCommented:
You are most welcome - good luck with it sounds interesting.
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
PHP

From novice to tech pro — start learning today.