Mixins for PHP

Posted 2 May 2002 at 04:46 UTC by whytheluckystiff Share This

Since I've started working on Psychoo, a number of people have contacted me about the PHP code behind the system. We talk all about hacking PHP's classes. In the process of toying with the language constructs, I found that there is a technique for implementing mixins.

I find mixins very useful in other languages. In Ruby, any class that implements the each method can utilize the Enumerable mixin. Enumerable is a class containing functions which use the each method including: find, grep, and reject. The functions are mixed into the new class's set of functions. Incidentally, that PHP does not implement mixins natively is not a criticism of PHP. PHP's design goals are different from Ruby's.

So how are mixins allowed in PHP? We can thank equal parts class methods, introspection, and dynamic function generation. Oh and I'm working with PHP 4.1.0+.

Class Methods

One other developer I communicate with asked me to show him the syntax for calling methods of a parent class within a child class. I worked up this example for him:

class Person {
    var $job = "person";
    function show_job() {
        echo "Hi, I work as a {$this->job}.";
    }
}

class Bartender { var $job = "bartender"; function show_job() { echo "BARTENDER: "; Person::show_job(); } } $b = new Bartender; $b->show_job();

The example worked just as I had eagerly anticipated, printing:

BARTENDER: Hi, I work as a bartender.

I scanned over the code once, ready to drop it in an e-mail. But then I noticed that I hadn't actually extended the Bartender class from Person. But the program worked perfectly. How could this be? Well, of course, you may already know this but class methods can be called anywhere, not just in a derived class. What's better, when run in the context of an instance of another class type, the method still uses $this properly. I just hadn't seen it spelled out for me like this. I simply assumed differently.

Oh, this is great I thought. I just need to find a way to introduce wrapper code into each class. I envisioned a mixin function that would take a class and mix it into a class like this:

class Bartender {
    var $job = "bartender";
    mixin( "Person" );
}

The Bartender class code produced would then look like this:

class Bartender {
    var $job = "bartender";
    function show_job()
    {
        return Person::show_job();
    }
} 

Of course, running a function right inside a class like I show with the mixin function above isn't possible. Class declarations must be pure variable and method declarations. Knowing this, I speculated on other possible contexts for executing the mixin function.

// Mixin through the constructor...
class Bartender {
     function Bartender() {
        mixin( "Person" );
    }
}

// Mixin through an instance... singleton... $b = new Bartender; mixin( $b, "Person" );

I figured one of these would work out. With enough willpower, perhaps more than one.

Introspection

The first step in mixing in functions from another class is to get a list of the functions, so we can clone them. PHP has a healthy list of introspective functions, one of which is get_class_methods. This function takes a class name and returns an array of the names of the functions defined for that class. So, the basic recipe for our mixin function will loop through the get_class_methods for a to-be-mixed-in class, generating wrapper functions inside the new class.

Dynamic Function Generation

Hopefully you can understand that I didn't exactly want to use eval(). I knew that there were other ways of generating functions dynamically. I found create_function, but it was a glorified eval(), accepting the function code as a string. Also, the create_function couldn't add methods.

One thing create_function could afford us, though. It would help us mixin through an instance.

$b = new Bartender;
$b->show_job = create_function( '', 'return Person::show_job();' );
$b->show_job();

This won't work properly, though. Close but no cigar. If you echo the $b->show_job variable, you'll see it print: lambda_1. That's because create_function actually creates the function lambda_1, so that when you run the variable as a function (i.e. $func_var();), it just performs a variable substitution and runs lambda_1.

As a result, I haven't yet found a way to mix in with an instance, singleton-style. Feel free to trump me by showing me a way around.

So it was back to eval(). I started with a useful function that I knew was possible at this point. The create_mixed_class takes a string for a new class name and an array of classes to mix into the class, in order of precedence. This function creates the new class, adding wrapper functions for the methods of the other classes. Note there's no error-checking here.

function create_mixed_class( $new_class, $based_on )
{
    $code = "
        class $new_class
        {";
    $func_code = array();
    foreach( $based_on as $b )
    {
        foreach( get_class_methods( $b ) as $bm )
        {
            if ( isset( $func_code[$bm] ) )
                continue;
            $func_code[$bm] = "
                function $bm() {
                    $args = func_get_args();
                    eval( '$ret = $b::$bm( $args['
                        . implode( '], $args[', array_keys( $args ) )
                        . '] );' );
                    return $ret;
                }";
        }
    }
    $code .= implode( " ", $func_code ) . "}";
    eval( $code );
}

This doesn't exactly let us mix into a class we're defining. But we can create a MixedBartender class, which acts as a proxy class between the classes:

create_mixed_class( "MixedBartender", array( "Bartender", "Person" ) );
$b = new MixedBartender();
echo $b->show_job();

My example at this point is rather awful, as it's still uni(?)-morphic. And you're probably thinking that this isn't really in fact a mixin as the title promised and that you've wasted your time. Well, I can do a bit better. Just a bit. I can give you a function to give you an instance of mixed classes.

The new_mixed_class takes an array of class names. A new, "anonymous" class will be generated with create_mixed_class. Then, we'll pass back an instance. In case you don't want to come up with names for your mixed classes. No, I didn't get this idea from create_function.

function new_mixed_class( $based_on )
{
    $mixed_name = "mixed_class_" . implode( "_", $based_on );
    if ( !class_exists( $mixed_name ) )
        create_mixed_class( $mixed_name, $based_on );
    eval( "$m = new $mixed_name;" );
    return $m;
}

Yep, had to use eval again.

Conclusion

So ultimately, I got close to the goal and brought some fraction of the mixin philosophy to PHP. I planned on using mixins for the next version of Psychoo. These two mixin functions give me what I need.

Yet, I had hoped to find a way to add mixins within the class declarations. Who knows. Maybe it'll work out someday.

(A copy of this article is also archived here at the Javuh Codelab.)


what is the design goal of PHP?, posted 2 May 2002 at 21:34 UTC by sye » (Journeyer)

Pardon me for asking a dumb question. What is the design goal of PHP? Matz's presentation spells out his design goal for Ruby. Your writing on this mixin technique sounds more like confession to me. PHP isn't worth the title of scripting language . We can do away with it quite easily.

There's supposed to be a design goal?, posted 3 May 2002 at 01:50 UTC by amars » (Journeyer)

From my understanding, PHP (as most technologies) resulted from the fact that there wasn't a better tool out there to do something, and it evolved from there. The fact that someone didn't sit down one day and say "I want to design a language and it's going to do this, this, this, and this and it's going to be like these languages" doesn't invalidate it as a language. That's not to say there wasn't planning along the way though. PHP is especially suited for web scripting, and if there is/was ever a goal to PHP, that would be it, and has succeeded marvelously.

Simplicity, a goal, posted 3 May 2002 at 03:39 UTC by whytheluckystiff » (Master)

PHP was sort of based on other nomenclature in common langauges. I'd say it's closest cousins are Perl and Java. Loosely, of course. From these languages, the syntax was simplified. PHP classes are the simplest form of a class: single ancestor objects with properties and methods. Immutable after creation.

I suppose that's the confession. Would the simplicity of the language obstruct me from adding a foreign principle to the code, without breaching the internals?

It's like this chunk of PHP code, which I thought could be leveraged to add mixins in the constructor:

class Bartender {
    function Bartender() {
        function show_job() {
            return Person::show_job();
        }
    }
}

How would you expect the above code to behave? We have a function nested with a class method. I see that it's within the scope of the class, so I figured the class would have two methods. Instead, the class gets the constructor, while the show_job() function is rooted in the global scope.

Why? Because it's forgiving for a simple user. A simple user will only ever nest functions if a script is being included within a class method. In those cases, the user is expecting the function to be introduced into the global scope. It's what's aggrevating about PHP, but it's also what makes it challenging.

Design Goal, posted 3 May 2002 at 13:00 UTC by rasmus » (Master)

PHP's design goal from the very beginning is very simple. To solve the common web problem. That's it. People always look for more grandiose motivations, but they really aren't there. It is a simple tool to solve a simple problem. There are way too many overly complex tools out there to solve what is essentially a very simple problem.

And yes, the language itself by some standards is somewhat primitive, but then it was never designed to be a revolutionary language. The amount of academic masturbation has been kept to an absolute minimum, focusing instead on solving real-world problems.

With over 9 million of the 38 million domains that Netcraft sees using PHP, it would seem that quite a few people like such a simple tool and I don't really understand the desire to "do away with it".

my apology..., posted 3 May 2002 at 18:25 UTC by sye » (Journeyer)

for not making myself clear. By "doing away with...", what i mean is that you keep your beard and i'll keep my long hair; you cut your hair and i shave my legs. If i can't get rice in the local grocery store or if i can't buy java in the corner coffee shop, i'll learn a new language.

I only wish whoever feel the need to re-invent yet another programming language can identify those real problems they are facing and for how long they've suffered for not having a remedy and what side effects their new remedy might bring to the unsophisticated populace.

Worse is Better, or how languages become popular, posted 3 May 2002 at 20:03 UTC by raph » (Master)

sye: what you seem to be saying, in your usual cryptic way, is that you prefer the "better" position in the Worse Is Better debate. Another excellent paper on this topic is Paul Graham's Being Popular.

I think what lies at the bottom of this issue is the fact that designing good programming languages is hard. Language designers tend to focus on either elegance or usefulness; much more rarely both. I get the impression that PHP is unapologetically in the latter camp. I don't believe there is anything wrong with this. If nothing else, the two camps have a lot to learn from each other; indeed, how else would languages that are both useful and elegant come about?

Errata, posted 3 May 2002 at 21:58 UTC by whytheluckystiff » (Master)

The code for the create_mixed_class and new_mixed_class functions above won't work if you cut-and-paste them, as some of the backslashes in the code were stripped when I submitted the article. Updated versions of the code are available at the Codelab article or you can find the "Mixins" section of Javuh.inc in CVS (which abstracts out alias_method).

at best..., posted 5 May 2002 at 12:13 UTC by sye » (Journeyer)

At best, PHP is like nails and duck tapes in a carpenters tool box. Nowadays, carpenters use Perl as hammers to get their jobs done. Mod_virgule is like a pair of pearl earings, whoever loves it can just grab it from her carpenter and need almost no assistance from him to put it on and looking good. What's more, among her friends who has the taste for good things, they would exclaim "where did you get that pearl? It shines brighter than the one i bought from my jeweler which cost me fortunes." To which, the lady replies, "my dear friend, not everyone has the gift to make pearls out of a carpenter's toolbox. i'll give you the name of my carpenter and you owe me a big favor".

what?, posted 5 May 2002 at 16:35 UTC by amars » (Journeyer)

That makes no sense and is totally out of context. If you don't like PHP, it is undoubtedly because you don't have a use for it... fine, don't use it. Nobody cares. But making up stupid pointless analogies about carpenters and pearls to put up on pedestals completely unrelated technologies only makes you look like a fool.

in Ancient China..., posted 5 May 2002 at 19:16 UTC by sye » (Journeyer)

amars, in Ancient China, wars have been fought and blood shed for thousand years so that the ancient country can speak one language and use one currency under one ruler. i don't mind you saying that i look like a fool since it makes you more like a fool yourself.

and yet, posted 5 May 2002 at 20:38 UTC by amars » (Journeyer)

China is still split between two distinct languages. The spoken language analogy aplies here. If there were one universal language that could be used to express any emotion and be used to communicate objectively between any number of people, by all means, the rest of the world should speak it. But there isn't, and chances are there never will be, as each individual language is evolving at all times, it is unlikely that all languages will one day evolve into one universal. The english i speak today, wasn't the english i spoke a decade ago and it's not the english that my parents spoke, or even speak now. The same goes for programming, there are a million different ways of doing a million different things. Just because you can't do with PHP what you need done, they way you want it done, doesn't mean it should be "done away with".

Granted there are faults in PHP, as are in any language, yes even perl and ruby. No one is holding PHP on a pedestal and saying it's revolutionary and can do everything the way everyone wants it to. And there are a million situations where i would not use PHP and there are a million situations where i wouldn't use anything but PHP. It serves a purpose. No one says you have to use it or even like it. If holding that opinion makes me foolish, so be it. But criticising it because (i assume) you don't have a use for it is just stupid.

PHP flame war, posted 6 May 2002 at 13:46 UTC by brlewis » (Journeyer)

What a bunch of interesting issues brought up here! What is PHP really good for? How newbie friendly is it? What do those usage (installation?) statistics really mean?

I'd love to post more on such issues, but I think we're diverging too much from the topic of the original article. If someone posts an article of PHP advocacy or critique, I'll gladly reply there.

Here, I think it would be appropriate to discuss just those strengths/shortcomings of PHP that are germane to the article.

Aggregation, posted 7 May 2002 at 20:01 UTC by andrei » (Master)

Interestingly enough, the current CVS code of PHP contains a few functions that allows one to dynamically add (mixin) methods from other classes into an object. I added them after some brainstorming with Stig Bakken, but apparently the interface will be simplified and integrated into PHP 5.

For now, though, you can do something like:

$b = new Bartender;
aggregate($b, 'Person');
$b->show_job();
Another approach is to call aggregate() in Bartender's constructor.

Ah, recompiling..., posted 7 May 2002 at 23:32 UTC by whytheluckystiff » (Master)

Thanks for the reply, Andrei. I'm just enabling the aggregate and overload functions now. It's funny.. this is exactly what I was looking for. I see that it's been rather tightly railed on the PHP-Dev list recently. Surprising that you would recommend it after this rebuking:

If aggregate() functionality is being used right now, and is critical, it should be kept, properly documented, the drawbacks spelled out in all its ugliness and its use strongly discouraged. It should be clearly marked as obsolete from day 1, and may be removed at any time. If someone uses this feature despite the warning labels, this person deserves everything that happens.

I'd say his claims are rather brash. It seems the serialization issue could be addressed by internally adding the aggregate to the internal __wakeup function or by allowing serialization to cooperate by understanding aggregated(?) objects. I don't know PHP internals enough to know if the __wakeup hook is possible. As for introspection, you could solve this with his same interface remedy. A get_class_aggregates() that returns any aggregate classes attached to the object.

I don't know if aggregate() is the way to go. Mixing into an instance is nice for singletons, but mixing into a class declaration is a lot more useful and less redundant. And aggregate( $this, "Classname" ) in the constructor doesn't count, as it still ends up being an instance mixin. Not only that, but it's difficult to say which solution is truer to PHP's form. I had no clue there was so much heat around the issue.

Nice work, Andrei. The overload() function is awesome. I hope it (or some type of accessor handling) is introduced into the main trunk of PHP soon. This is exactly what I've been trying to do with Psychoo. Objects loaded from the database could be accessed like this:

$person = $psychoo->load_one( array( "person", "category" ), $idx );
echo $person->name;
#=> Andrei
echo $person->name['type'];
#=> varchar

Then for link fields:

echo $person->category_idx;
#=> 14
echo $person->category_idx['link'];
#=> Category object

This is enjoyable syntax.

It fills a need, posted 8 May 2002 at 19:24 UTC by andrei » (Master)

I don't know if aggregate() is the way to go. Mixing into an instance is nice for singletons, but mixing into a class declaration is a lot more useful and less redundant. And aggregate( $this, "Classname" ) in the constructor doesn't count, as it still ends up being an instance mixin. Not only that, but it's difficult to say which solution is truer to PHP's form. I had no clue there was so much heat around the issue.

whytheluckystuff, I think it's actually good that aggregation has caused such an uproar; it means that people care about it. Sometimes introducing code into CVS is the only way to get the discussion started, and as a result of this particular one there will be a way in PHP 5 to aggregate methods into classes. Although, doing the same for instances might not be possible until some later version.

Nice work, Andrei. The overload() function is awesome. I hope it (or some type of accessor handling) is introduced into the main trunk of PHP soon.

Glad you like it. The ZE2 overloading is a lot better, but the extension will need to be rewritten from scratch.

-Andrei

OK. i want to learn Arabic Fonts, posted 9 May 2002 at 15:29 UTC by sye » (Journeyer)

Ok. taking amar's advice, i add myself to Arabic Fonts for Linux project. If one day, i'm captured by Osmar Bin Laden, by showing "Works of Mencius" in Arabic Fonts, i may have a chance not to go to see his Allah but go home and care for my son.

instance_mixin(), posted 11 May 2002 at 23:55 UTC by whytheluckystiff » (Master)

Thanks to a useful response on the Javuh-develop mailing list, we've completed the set of mixin functions I had hoped for in the article. The instance_mixin function will take an instance and mix it into a new class. This isn't exactly the way I envisioned solving the problem, but it's a way to do it and it's bound to be more controversial that the aggregate function. But as long as people understand that this function creates a class that doesn't exactly fit into PHP's normal types of objects...

    /**
     * Mixes an instance into a new class [Thanks, WorldMaker!]
     * @param $this An object, the instance
     * @param $based_on Classes to mix together
     */
    function mixin( &$this, $based_on )
    {
        array_unshift( $based_on, get_class( $this ) );
        $mixed_name = "mixed_class__" . implode( "__and__", 
$based_on );
        if ( !class_exists( $mixed_name ) )
            create_mixed_class( $mixed_name, $based_on );
        $vars = get_object_vars( $this );
        $this = new $mixed_name;
        foreach( $vars as $k => $v )
        {
            $this->$k = $v;
        }
    }

So quite similar to aggregate(), but not as fast I'm sure, though parent classes are listed in the class name, which is... something I guess.

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!

X
Share this page