Chris Harrop from Acquia introduced their devcloud platform and some of the other tools and services they offer.
Murray Woodman presented some performance measurements of node loading when using Entitycache and other techniques.
Justin Randall gave a pretty thorough introduction to using XHProf to detect and identify performance problems and regressions.
Chris’ talk about Acquia’s offerings was very short and quite high level. I was interested to see their “checklist-style” Drupal evaluation and testing tool as I’d was looking at a similar sort of run-the-tests-in-this-checklist tool yesterday.
Also: I got a code for a free dev environment on their cloud service.
The bulk of Murray’s talk was to present benchmarks of several approaches to
improving entity_load
performance in Drupal sites.
He compared:
Setting innodb_buffer_pool_size
appropriately to your machine and
workload.
Enabling the MySQL query cache.
Installing the Entitycache module.
Using APC to store cached objects.
Several combinations of the above.
The biggest and quickest win was installing entitycache (which resulted in an “80%” performance improvement), with the other techniques yeilding smaller improvements.
Justin began his presentation with a quote:
Don’t bring logic to a data fight.
Analysing system performance and identifying performance problems can be complex and real data on specific cases is more useful and more reliable than logic. “Don’t trust your gut, use science.”
After a bit of good advice about performance – it’s a process not a goal; test, and fix, client and network issues first – Justin dug into using XHProf to record and analyse performance profiles of Drupal pages.
The XHProf extension has so small an impact on performance that you can run it on production servers ready and waiting for a performance problem to profile (which degrades performance by around “<10%”).
Along with the XHProf extension are several tools to process and analyse the recorded profiles:
The moderately awful web interface which comes with the extension.
The XHProf Drupal module.
Mark Sonnabaum’s XHProf CLI tool to aggregate multiple profiles so that runs can be compared more reliably (when, e.g., checking for performance regressions in a patch).
A good tip, which hadn’t occured to me, was to use the auto_prepend_file
feature in PHP to enable XHProf. This is application agnostic, does not require
changes to the application code, and ensures that your profiles include the
whole application.
The idea of building XHProf into my production environments, with a prepend file configured and waiting to enable profiling (or automatically profiling, say, 1% of requests) is intriguing.
So this was a pretty good introduction to the Drupal community in Sydney, and it was nice to see how other groups run meetups (though I gather there have been some recent changes here).
]]>mod_php
embedded in Apache. This is fine in most cases, but sometimes it’s definitely
the wrong thing to do; in my case, I’m processing large image files (~4M or
so) and keeping my memory limit down. The easiest way to do this is to offload
the processing to another process. Something like ImageMagick perhaps? With a
custom action from ImageCache Actions, this is simple!
A quick Google led me to an article called Create PDF thumbnails with imagecache and ImageMagick while GD is still the default toolkit which supplies the following code (simplified a little by removing the PDF-y bits):
<?php
$w = 246; // change to your preferred thumbnail width
if (!_imageapi_imagemagick_convert($image->source.'[0]', $image->source.'.png', array(0 => '-thumbnail '.$w))) return FALSE;
$img = imagecreatefrompng($image->source.'.png');
$image->source.'.png');
file_delete($image->resource = $img;
$image->toolkit = 'imageapi_gd';
$image->info = array('extension' => 'jpeg');
return TRUE;
?>
Alas, this code it pretty useless: because it stomps on $image->info
any
further actions on this $image
will probably break.
Thankfully, there’s an easy fix: when you update $image
, make sure you
update everything that needs fixing. Here’s the amended code:
<?php
// "Thumbnail" the image
$width = 600;
if (!_imageapi_imagemagick_convert($image->source, $image->source.'.png', array(0 => '-thumbnail '.$width))) return FALSE;
// Load it back in as a GD resource
$img = imagecreatefrompng($image->source.'.png');
// Get the "deets" on the new image
$info = getimagesize($image->source.'.png');
$image->resource = $img;
$image->toolkit = 'imageapi_gd';
$image->info = array(
'width' => $info[0],
'height' => $info[1],
'extension' => 'png',
'file_size' => filesize($image->source.'.png'),
'mime_type' => $info['mime'],
;
)
// Clean up
$image->source.'.png');
file_delete(
return $image;
?>
Make sure that you’ve enabled the ImageMagick toolkit, drop this code in a custom action and you’ll be on externally processing images in no time!
]]>Attributes are “extra” bits embedded in source code that doesn’t necessarily mean anything in the language. Common examples include the documentation annotations in many comment systems (such as the tags used in Javadoc comments), JVM annotations, and .Net attributes.
A testing framework, for example, might use a TestFunction
attribute to
identify which methods should be called.
@TestFunction
public function testValidation() {...}
[TestFunction]
public function testValidation() {...}
In Java EE you have “beans” which encapsulate business logic. Each bean requires a bunch of wrappers for remote access, “home” access (whatever that means), XML files, etc. Keeping all this relatively “boiler-plate” code in synch is painful. Annotating the bean classes and methods with attributes makes it possible to generate most of this automatically. No more manual synching XML files.
Java, though, (as of Java 5) has built in support for annotations and is a compiled language. This makes it relatively simple to do this analysis and generation at build time. PHP, on the other hand, doesn’t have an annotation mechanism and doesn’t have a built time.
Instead, Peter’s approach is to use PHP comments to add the attributes. At load time, these comments are processed using the reflection API and this is used to build a data structure to use for access control. See the permissions example.
To take this a little further, Peter wrote a Zend
extension that makes it
possible to [sort of] do aspect oriented programming (on function points) in
PHP. The add_function_hook()
function allows you to register an attribute
and a callback and the extension will call your callback before a function
with that attribute is executed:
<?php
class RequiresLogging
{
/**
* @log notice "Secret action performed by user [user]
*/
public function secretAction()
{// perform secret action...
}
}
//
// The callback function
//
function performLog( $message )
{$parts = split( " ", $message, 2 );
$level = $parts[0];
$message = $parts[1];
log( $level, $message );
Logger::
}
//
// Register the callback
//
"log", "performLog" );
add_function_hook(
// Now code like this will result in a log message being recorded
$object = new RequiresLogging();
$object->secretAction();
This type approach is flexible and extremely powerful. You can inject permission checking code and raise an exception before the real function is even called.
There are a bunch of memory leaks and possible bugs in the Zend extension, but it is working and in production use!
]]>He started with a list of things that have been removed or altered in PHP6 and then described some of the new features. Some of the things from both lists are present in version 5.3.
In PHP6 E_ALL
will include E_STRICT
which may cause errors to be reported
in code that is OK in PHP5. A new E_DEPRECATED
has been introduced and is
also included in E_ALL
. This new error is raised when you use any deprecated
functions.
These deprecated functions and features include register globals, magic
quotes, safe mode and ASP-style <% %>
tags. Also going/gone are the
$HTTP_POST_VARS
and $HTTP_GET_VARS
variables.
Other changes include assign-by-reference (using =&
) and zend_compat
raising E_STRICT
. Class members defined without a visibility modifier will
be public
by default (and will also raise E_STRICT
).
Dynamically loading extensions is now disabled by default on all SAPIs except
the command-line interpreter. This needs to be enabled (in php.ini
, I
assume) if required.
In a rather acrobatic backflip, the formerly deprecated $str[1]
is no-longer
deprecated and its replacement $str{1}
now is deprecated. You can also now
use the subscript syntax to take a slice of a string – like $str[1,3]
– as
you in some other languages.
The wacky variable break
feature (where one breaks out of a variable number
of looping constructs: break $anint;
) is deprecated. It’s disappearance make
me wonder (again) why it was added in the first place. I can’t help but wonder
if there was a use-case in mind, or if it was just “because”.
The microtime()
function now returns the float value by default; you’ll no
longer need to use microtime(TRUE)
in every invocation.
The move to using the PCRE-based preg_*
functions for regular
expressions is finally complete. The ereg_*
functions have been
removed from core and stuck in a PECL extension. At the same time,
mime_magic
has also been moved to a PECL extension and its
more useful and popular cousin fileinfo
has replaced it in core.
The big ticket new item must be namespaces. The controversial choice to use backslash as the separator was due to the simple fact that it is the only single character available. This doesn’t make it any less silly, especially because of the way that function “references” in PHP are just strings:
$func = "\application\names\afunc";
$func();
All names refer to the current namespace, but prefixing a name with a backslash will make it relative to the “root” namespace.
You can import things from other name spaces with the use
keyword and, if
required – perhaps there is a clash – alias them by taking an as
clause on
the end:
use \MyCompany\Blog\User as BlogUser;
use \MyCompany\CMS\User as CMSUser;
Functions and classes both belong to namespaces and definitions are processed
top-down. Thus the following code defines a foo()
function and then a
bundle\foo()
function:
function foo() { echo "G"; }
namespace bundle;
function foo() { echo "NS"; }
; // "NS"
bundle\foo(); // "G" foo()
Another important change is the addition of late static binding. In previous
versions of PHP self::foo
in the code of a superclass method refers the
foo
member of that superclass, even if the subclass defines a foo
member.
This problem is ameliorated with the addition of a new static
keyword.
static::foo
will do what you expect: look in subclass, then in the parent/s.
All in all, it looks like PHP6 is a great big leap forward in the evolution of PHP. A few more major versions and it might catch up to modern languages.
]]>The problem, it seems, lies in SPIP’s use of locking to serialise
access to a number of files containing cached data. In essence: SPIP
seems to deadlock when attempting to lock certain files in the /tmp/
directory. This hangs the PHP process (FastCGI, in our case) which
consumes resources and, eventually, prevents the web-servers from
being able to serve requests for PHP. To make a bad problem even
worse, we seem to be the only people to experience this problem.
Literally no-one else in the SPIP community has ever encountered it!
As a first step in trying to diagnose and resolve the problem, I’ve
instrumented the code that implement’s SPIP’s locking techniques to
log all locking operations. Thankfully, this is relatively easy as
SPIP wraps the actual flock
calls with its
own spip_fopen_lock()
and spip_fclose_unlock()
functions defined
in
/ecrire/inc/flock.php
.
I can’t use the built in SPIP logging functions because they use the
locking functions I’m trying to instrument to prevent interleaving of
log messages. The obvious choice (and far superior to spip_log()
, in
my opinion) is using the syslog
protocol supported by pretty much everything that matters (i.e.
everything UNIX and lots of networking gear). Alas, PHP’s built-in
syslog()
function logs to the local syslog
daemon. Instead, I’ve used a small class that implements a Pure PHP
syslog client.
Rather than copy-‘n’-paste a bunch of code into the SPIP locking
functions, I’ve created a function to figure out what the message
should be1 and then log it. logtrace()
creates a PHP backtrace
and inspects it to determine which function (lock or unlock) it is
logging and which function (of the dozen or so alternatives) called
it. Once it’s figured that out, it uses a static instance of the
Syslog class to send the message to the log server. The code is
simple:
<?php
require_once('syslog.php');
@define("_SYSLOG_SERVER", "192.168.1.1");
function logtrace() {
// The syslog client
static $log;
if (! $log) {
$pid = getmypid();
$log = new Syslog();
$log->SetProcess("PHP-$pid");
$log->SetIpFrom($_SERVER['REMOTE_ADDR']);
}
// Get the functions from the backtrace
$bt = debug_backtrace();
$callee = $bt[1];
$caller = $bt[2];
// Build and log the message
$func = $callee['function'].'('.$callee['args'][0] .')';
$from = $caller['function'].'('. $caller['args'][0] .')';
$log->Send(_SYSLOG_SERVER, "$func from $from");
}
With that defined, it’s just a matter of adding a call to logtrace()
to spip_fopen_lock()
and spip_fclose_unlock()
in /ecrire/inc/flock.php
and we get log messages something like this2:
Nov 3 13:54:07 server2.example.com WEBSERVER PHP-1234: www.example.com 10.10.10.10 spip_fopen_lock(../tmp/verifier_plugins.txt, ...) from lire_fichier(../tmp/verifier_plugins.txt, ...) Nov 3 13:54:07 server2.example.com WEBSERVER PHP-1234: www.example.com 10.10.10.10 spip_fclose_unlock(Resource id #69, ...) from lire_fichier(../tmp/verifier_plugins.txt, ...)
Of course, this is not enough information to debug the locking problems – I’ll also need the IP address of the client and the PID of the PHP interpreter to distinguish interleaved messages from concurrent requests – but it’s a start.
<br/>
s, for
example). Thankfully, the “shortcuts” are implemented as a “treatment”
for the #TEXTE
, etc. tags and can be overridden quite easily.
In this post I’ll describe how SPIP’s “treatments” work and how they can be overridden. In the process, you’ll see how to replace the built-in typographical shortcuts with my favourite light-weight mark-up language – PHP Markdown Extra and Smartypants – as well as a few less intrusive “tweaks”.
SPIP’s template language is based on the idea of loops – which “loop” over the results of a database query – and “tags” – which output the value of a column from the current row1. If all you want to do is loop over the contents of a database table and output some values then you don’t have to “do” anything:
<BOUCLE_aloop(my_table){par acolumn}>
#ACOLUMN
#ANOTHER_COLUMN</BOUCLE_aloop>
SPIP will do what you want without any additional code of
configuration (assuming that your table is called my_table
and has
columns called acolumn
and another_column
). This “default”
behaviour (assuming that loops and tags match with tables and columns)
makes it very easy to add new loops and tags: just modify the database
schema and it all “works”.
There are lots of circumstances, though, where some additional processing will need to be done on values before they are suitable for display. Perhaps some special characters need to be escaped or some formatting applied before the data it is suitable for output. You could just apply a filter to the tag each and every time you use it:
[(#ANOTHER_COLUMN|process_data)]
Or implement the tag in PHP:
function balise_ANOTHER_COLUMN_dist($p) {
$p->code = 'process_data('
.champ_sql('another_column', $p)
.')';
return $p;
}
Thankfully, you don’t have to filter the tag in every template, or
implement the tag in PHP to use such “treatments”: you can just tell
SPIP what function/s to call with the $table_des_traitements
global
variable.
The $table_des_traitements
variable is an array of arrays (hence the
“table
”) which SPIP uses to figure out exactly how to process a raw
database value into something that is safe to output in a page. The
“first” dimension of the array is the tag name – TEXTE
, DATE
,
etc. – and the second is the loop – documents
, articles
, etc.
When SPIP is evaluating a tag (whether or not it has been implemented
in PHP), it checks the $table_des_traitements
array to see if there
is a “treatment” for that tag/loop combination or, failing that, for
the tag everywhere (the 0
th element in the array). This happens in
champs_traitements()
and takes the “star” into account (where adding a *
to the end of
a tag suppresses automatic filtering).
You can see the default values in
ecrire/public/interfaces.php
,
but lets consider the trivial example of removing the leading number
from all #TITRE
s with the supprimer_numero
filter (completely
ignoring the normal processing):
global $table_des_traitements;
$table_des_traitements['TITRE'][] = 'supprimer_numero(%s)';
Another trivial example might be to transform the #TEXTE
of all
sections (but not articles, etc) to uppercase2 (again ignoring the
normal processing):
global $table_des_traitements;
$table_des_traitements['TEXTE']['rubriques'] = 'strtoupper(%s)';
Getting back to my goal of replacing SPIP’s built-in typographical
shortcuts language with the PHP
Markdown and
Smartypants. The
TEXTE
entry in interfaces.php
has the value 'propre(%s, $connect)'
. Replacing this call to propre()
(which implements the
typographical shortcuts) is simply a matter of installing the
markdown.php
and smartypants.php
scripts and adding a similar line
to our mes_options.php
file:
global $table_des_traitements;
include_once "markdown.php";
include_once "smartypants.php";
$table_des_traitements['TEXTE'][] = 'SmartyPants(Markdown(%s))';
Now the value set in interfaces.php
will be
$table_des_traitements['TEXTE'][1]
instead of
$table_des_traitements['TEXTE'][0]
and our new value will be the
default!
Alas, there is one very large drawback to doing this. While I’ve
replaced the very limited typographical shortcuts with the much more
powerful Markdown, this change has also removed the ability to embed
models in the TEXTE
of articles, sections, etc. It’s now harder to
use images and documents within texte
and completely impossible to
embed forms and other interactive elements.
This is a serious drawback which I’ve still not figured out how to work around.
Please note that I am grossly oversimplifying things here: loops can loop over other data, and tags can do much, much more than merely output values from a database. But this is the basis for much of the design and implementation and is the default behaviour.↩︎
And thus probably breaking your document as XML tag and attribute names are case sensitive and XHTML defines them all as lower-case.↩︎
autoriser()
–
the function which determines whether a user should be permitted to perform a
given operation. As I couldn’t find any document on using this function, I’ll
provide a few notes below.
Checking that a user if authorised to perform an action is a little more
haphazard than I’d like in SPIP (“No authorisation check? No worries!” is not
a particularly comforting approach), but it seems to get the job done even if
it does depend on more developer discipline than seems warranted. Checking
that a user is “authorised” to perform an action is done by calling the
autoriser()
function with arguments to
describe the operation. If it returns true
then the operation is authorised,
it not, then it isn’t.
Like most of SPIP’s core functions, autoriser
is implemented in a way that
makes it easy to override and extend its functions: rather than make any
decision itself, it simply delegates the decision to the first function it
finds that can decide for that type of operation and object (or object, or
operation).
First, though, lets look at autoriser
’s arguments:
function autoriser_dist($faire, $type='', $id=0, $qui = NULL, $opt = NULL)
This first (and only required) argument is $faire
(French, I’m told, for “to
do”) which takes a string: the name of the operation. The second argument
$type
is another string: the type of object being operated on; and the third
is an integer: the ID of the particular object, if there is one. The fourth,
$qui
(“who”) is an array of details of the current user; and the fifth, I
assume, is an array of optional values if the previous four are not enough to
make some decisions).
Only the first of these – the operation being performed – is required and
only the first should need to be specified in the vast majority of situations
(it’ll work out the user by itself and there are many operations without a
$type
or an $id
). Once it’s been called, autoriser
uses these values to
look for a function that can make a decision for the given type and
operations.
You can see the code of /ecrire/inc/autoriser.php
(around line
87)
for the particular functions that it will call, but the full list of
alternatives that ‘’autoriser($faire, $type, $id, $qui, $opts)’’ is (in order
of preference):
autoriser_$type_$faire()
autoriser_$type()
autoriser_$faire()
autoriser_default()
autoriser_$type_$faire_dist()
autoriser_$type_dist()
autoriser_$faire_dist()
autoriser_default_dist()
Adding authorisation checks to your plug-in is easy: just implement one of
these checking functions (in a file that’ll be included by a <fonctons>
entry in your plugin.xml
file is probably best) and then get autoriser
to
call it when appropriate.
From aplugin_fonctions.php
or some other file:
/**
* Perform authorisation checks for "elephant" objects.
*/
function autoriser_elephant($faire, $type='elephant', $id=0, $qui=NULL, $opt=NULL) {
if ( '0minirezo' == $qui['statut'] ) {
return true;
}return false;
}
With this code in place, only administrators will be able to perform actions
(or, strictly speaking, perform actions checked with the autoriser
function)
on elephant objects. Any call specifying $type='elephant'
will use the
above function to determine if the operation should proceed.
In exec/anaction.php
or similar, we might use code like this:
if ( autoriser('kill', 'elephant', $id_elephant) ) {
'elephant', $id_elephant);
launch_missiles_at(else {
} echo _T('aplugin:cannot_shoot_elephant'), _T('aplugin:permission_denied');
}
Which will try the following functions, in order, to decide whether or not to
launch_missiles_at()
our poor elephant:
autoriser_elephant_kill()
autoriser_elephant()
autoriser_kill()
autoriser_default()
autoriser_elephant_kill_dist()
autoriser_elephant_dist()
autoriser_kill_dist()
autoriser_default_dist()
That’s about all there is to it. Of course, there’s a lot more you can do to make your authorisation decisions: per-user and per-object configurations you might like implement (similar to the way SPIP allows us to restrict administrators and editors “to a section”), time-based or geographic restrictions (editing during working hours only, or from an IP address in Africa), or restricting access to those within your organisation’s network.
The world of authorisation is your oyster!
]]>#INCLURE
and #INCLUDE
) and then I’ll build on this technique, my previous post
Dynamic tags, fake arguments, and AST mangling in
SPIP, and a pattern
used in SPIP’s code to create suites of tags with multiple similar but
slightly different functions.
As usual, you’ll need to know PHP, and the SPIP template language before this will make much sense, and I’d recommend reading my posts about creating static tags, dynamic tags, and faking tag arguments as well.
SPIP tags are just a specially named function that accepts an AST node as a parameter and returns that AST node modified so that the code generator will generate the PHP code to implement the function. With static tags this means filling the node with some PHP code that evaluates to a string (often just a string literal with, perhaps, some variable interpolation). The functions for dynamic tags, on the other hand, fill their AST node with PHP code that, when evaluated by the code generator, generates more PHP. This second piece of PHP will form part of the page template and will [potentially] be evaluated at each request.
It’s sometimes desirable to make certain tags available under more than one
name. The canonical example of this from SPIP’s built-in tags is
#INCLURE
/#INCLUDE
. Rather than provide just the French #INCLURE
tag or
just the English (and PHP-ish) #INCLUDE
, SPIP provides both. This can help
make your API more user friendly and is easy enough to do: just define a
second tag function that calls the first (notice that I check for an
overriding version of my original tag implementation and call that instead):
/* The original tag */
function balise_FOO_dist($p) {
$p->code = "'This is foo'";
return $p;
}
/* The additional name */
function balise_BAR_dist($p) {
if ( function_exists('balise_FOO') ) {
return balise_FOO($p);
else {
} return balise_FOO_dist($p);
} }
If you’re looking for identical functionality under a different name (see
#INCLURE and #INCLUDE), this is all you need, but imagine that you,
like me, have a number of tags with related but slightly different
functionality. You could implement each of them – copy-and-pasting the code
and changing perhaps a single variable – or extract the common code into a
set of library functions – slowing things down slightly – or you could
implement a generic tag and a number of wrappers around it. SPIP does this in
a number of places, but the best example is the #ENV
, #CONFIG
and #GET
tags, each of which are actually implemented by the same piece of code (the
balise_ENV_dist
function, as it turns out).
This is easily accomplished by adding a second, optional, parameter to your
main tag function and then passing different values from your stub tags. This
is the approach that #ENV
, #CONFIG
, and #GET
use:
balise_ENV_dist
check the environment by default, but if it’s passed a
second parameter (the '$GLOBALS["meta"]'
that #CONFIG
passes it, for
example), then it operates on that instead.
A second, but rather more involved, option for passing values into other tags
is described in my faking tag
arguments post. This
can be useful when you don’t control the code that you are wrapping. Perhaps
you want to modify the #MODELE
tag in such a way that it adds the name and
line number of the tag to the environment of the model it calls. Rather than
duplicating the code for balise_MODELE_dist
and modifying it slightly, you
could write a wrapper around it balise_MODELE
and just add another two
parameters (skeleton
and line
) to the AST before calling
balise_MODELE_dist
. As far as balise_MODEL_dist
is concerned, it’s as
though some conscientious web-master has gone through every template adding
{skeleton=filename}{line=123}
to every call.
The difficult bit is to modify the AST correctly, but that’s not too hard and you can read the post to find out how.
This is a powerful technique that can simplify the code for plug-ins with large numbers of similar tags immensely. Rather than producing large amounts of “boiler-plate” code, or defining large libraries of API functions, I can just reuse my existing generic code in ways that hide it’s complexity and power, thereby making the API much more simple and obvious to my users.
]]>You should know about the SPIP, its template language, static tags, dynamic tags and be comfortable with PHP before reading this article.
Unlike static tags – which are implemented by a single function – dynamic
tags are comprised of a set of three functions (in a magically named file)
which are called by the SPIP engine as appropriate. This first of these three
functions (identical to the single function of static tags) is called
something like balise_FOO()
and is responsible asking SPIP to call the other
two functions: balise_FOO_stat()
– which performs whatever static
calculations are required – and balise_FOO_dyn()
which performs the dynamic
calculations and display the final content to the user.
An example might make this more clearer. Suppose I’m creating a #HITS
tag to
output the number of hits an article has received. My balise_HITS
function
arranges for SPIP to call the balise_HITS_*
functions like so:
It calls balise_HITS_stat
to determine the static parameters for the tag
(e.g. “id_article=36”).
It arranges to call balise_HITS_dyn
, passing it the static parameters as
determined by balise_HITS_stat
.
balise_HITS_dyn
uses the static parameters to query the database, etc.
and produces the appropriate output.
The code to implement the #HITS
tag might look like this:
function balise_HITS($p, $nom='HITS') {
$args = array('id_article');
return calculer_balise_dynamique($p, $nom, $args);
}
function balise_HITS_stat($args, $filters) {
return array($args[0]);
}
function balise_HITS_dyn($id_article) {
$now = date('c');
return "Article $id_article has had 1,000,000 hits as of $now!";
}
One way this can fall down is if I want to access details of, for example, the
SPIP templates in determining my static parameters. By the time I’m
determining the parameters, I no longer have access to the abstract-syntax
tree node passed to balise_FOO
. All I’ve got at this point, is an array of
arguments to the tag and an array of filters applied to the tag.
The obvious solution is to add the value to the “$args
” array I pass to
calculer_balise_dynamique()
. Alas, this will not work as $args
is not (in
spite of its usual name) an array of arguments. It is, in fact, an array of
the names of arguments which SPIP is to automatically fetch and pass on the
the balise_*_stat()
call. Appending the name of the template file
("squelettes/foo.html"
, for instance) to $args
tells SPIP to look for a
variable called squelettes/foo.html
in the context the tag is being used and
pass it through to the next function. Needless to say, this doesn’t work.
The correct solution to this problem is to arrange for SPIP to add the values
you want to the $arguments
array (or perhaps the $filters
array, but this
may not be a good idea). This array contains the values I requested in the
call to calculer_balise_dynamique
along with the values of the parameters
passed into the tag in the template. If I can’t use the former route, then
it’ll have to be the second – I’ll need to add a another “fake” parameter to
the AST node before I call calculer_balise_dynamique
. (Yes, I agree. This is
a rather odd way to accomplish my goal, but that’s how it goes in SPIP-land).
There are two parts of the SPIP AST that are relevant here: the Champ
nodes
that represent tags, and the Texte
nodes the represent “strings” (amongst
other things). The $p
that is passed to my balise_*
functions is an
instance of the Champ
class and I’m going to add a new instance of the
Texte
class representing my new “fake” parameter. Creating the new object is
pretty simple, just construct it and set it’s type
and texte
members. The
following example adds a new parameter containing the name of the template
file:
function balise_TEMPLATE($p, $nom='TEMPLATE') {
$file = $p->descr['sourcefile'];
// Create the new object
$t = new Texte;
$t->type = 'texte';
$t->texte = $file;
// Make sure that $p-param is an array (of the right dimensions)
if (! is_array($p->param) ) {
$p->param = array(array(0=>NULL));
}
// Append the object to the tag parameters
$p->param[0][] = array($t);
// Call SPIP's dynamic tags code
$args = array();
return calculer_balise_dynamique($p, $nom, $args);
}
First the previous code gets the name of the template file from the AST node
for the tag being processed. Then it creates a new Texte
object with the
filename as its value. It ensures that the $p->param
member of the Champ
object is an array (and yes, it does seem to start with a NULL
so that we
can pretend that the arrays are 1-indexed) and then appends the new object to
it. All that’s left is to call calculer_balise_dynamique
as usual.
With this done, the value of the new “fake” parameter will be evaluated and
passed to the balise_TEMPLATE_stat()
call in the $args
array and then (if
I choose) passed on to the balise_TEMPLATE_dyn()
call.
This technique still strikes me as a bit odd, but it’s the only way I can see
to implement this effect without introducing global variables. In my opinion,
calculer_balise_dynamique
should take an array of argument values as an
optional fourth argument, but needing to do this sort of thing is probably
fairly rare (though I have seen it in the code for one or two built-in tags).
Even if this technique is the “Right Way(TM)” to pass extra values around,
then it really does need a helper function like interprete_argument_balise
instead of mucking around with AST internals in every tag that need it.
Lots of SPIP tags and filters generate HTML mark-up as output (the #LOGO_*
tags and |image_*
filters are the most prominent) and we often need or want
to modify this mark-up in our templates. There are several built-in filters to
help with this
(inserer_attribut
to insert an attribute and
extraire_attribut
to extract the value of an attribute) but they are pretty long and tedious to
type. Given that I uses these filters quite often when implementing such
things as image galleries, and I still have to think quite hard to type the
French words correctly the can be a bit annoying. Thankfully, a small wrapper
(see below) can smooth these small annoyances away.
When it comes to accessing and modifying the values of attributes nothing, in
my opinion, comes any where near to jQuery’s attributes
API. The single jQuery attr
method allows you to get and set the
value for any attribute on any node: it takes one or two parameters, the name
of the attribute and, optionally, the value. If only the name is specified,
then attr
simply extracts and returns the value of the attribute. If both a
name and a value are specified, then attr
modifies the object setting
specified attribute to the specified value. This “polymorphic” style of
interface – where a single method has two complementary behaviours which are
distinguished by the number and/or types of the arguments – is everywhere in
jQuery and is part of what makes it so concise and so productive. Seeing as
it’s the best structure for such interfaces that I know (and also, one I use
daily), I decided that my wrapper should mirror it. Thus the attr
filter was
born.
Like the jQuery attr
method, the SPIP attr
filter takes one or two
parameters (and an implicit “object”, but we’ll ignore if for now). If only
one, it passes the input on to extraire_attribut
to get and return the
value. If called with two parameters, it calls inserer_attribut
instead to
modify the tag. Like the idea, the code is reasonably straightforward; the
only even slightly unusual but is the use of
func_get_args
to get an array of the arguments passed into the function call. With such an
array, we can use count
to
check how many arguments the function was given and decide whether we should
get or set the attribute. This is safer than specifying and checking a default
value (FALSE
or NULL
, for example) because some user may genuinely want to
use that value (perhaps NULL
will mean delete the attribute in a future
version?) and silently ignoring user input is never good.
<?php
"inc/filtres");
include_spip(
/**
* The `attr` function allows the user to get and set the attributes of an
* HTML tag. It is intended to be used as a SPIP filter and depends on
* existing SPIP functionality.
*
* @param $tag
* The HTML code.
* @param $name
* The name of the attribute.
* @param $val...
* The new value for the attribute $nom. Optional.
* @return
* If $val was given, the code for tag with $name=$val
* Otherwise, the value for the $name attribute in $tag.
*/
function attr($tag, $name){
$args = func_get_args();
if (count($args) > 2) {
// SET
return inserer_attribut($tag, $name, $args[2]);
else {
} // GET
return extraire_attribut($tag, $name);
} }
Simply copy the code into the mes_fonctions.php
file for your site (see
“Adding your own filters” in SPIP’s
Filters) and use attr
in your SPIP
templates:
<a href="[(#FICHIER|attr{src})]" class="lightbox" title="#TITRE">
[(#FICHIER|image_reduire{100,100}|attr{alt,#TITRE})]</a>
There are a few changes that could be made to this function: passing $args
directly to inserer_attribut
and extraire_attribut
rather than the
individual variables, adding a $value=FALSE
parameter for the sake of
documentation and then ignoring it (I’m not sure if this will work and don’t
care enough to try it), deleting attributes when passed e.g. NULL
as a
value, etc. For the time being, however, it does the job.
A final note: you’ll probably need to be running PHP 5 for this to work – the
func_get_args
documentation (linked above) mentions version 5.3.0 – and the
code above was modified after I last tested it, but should work anyway.