Mixins for PHP
Posted 2 May 2002 at 04:46 UTC by whytheluckystiff 
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.)
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.
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.
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.
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?
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.
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. 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.
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.