The page on content negotiation shows basic principles and presents links to other pieces of interesting information. Now we will see how and for what purpose content negotiation can be used in connection with pages generated dynamically by PHP. There are these possibilities:
This document will describe both methods. We will deal only with PHP but similar approach can be used with other scripting languages as ASP, Perl, Python, Ruby etc.
A PHP script is usually a mixture of program instructions using
both standard and user defined functions and an HTML code. The greatest part is
thus common for all language versions. We do not wish to create separate files
distinguished by extensions for all languages as we did in case of static
pages. We would like to make use of distinction by extension in order to make
content negotiation with MultiViews
work but we would like to
share the program code. Copying the code from file to file would easily lead to
errors and inconsistencies.
The PHP script shows texts in the WWW pages by means of the
echo
and printf
functions. These functions work with
strings which will have different values in each language version. Monolingual
version uses string literals. We will, however, use variables or constants
defined by the define
function. The program code will then appear
in a common file. Each language version will only define constants and
variables dependent on the language and then read the common file by the
require
function. The multilingual text can look as:
Czech file named file.cs.php
:
<?php define(_title_, 'Sjednání obsahu'); require 'incl.file.php'; ?>
English file named file.en.php
:
<?php define(_title_, 'Content negotiation'); require 'incl.file.php'; ?>
Common file named incl.file.php
:
<?php printf('<title>%s</title>', _title_); ?>
The corresponding page will be referred to just as
file
without any extension. Apache will then select the
appropriate variant.
Finally we must prohibit access to the common files because they would not work as such. This is achieved by this directive in the configuration file:
<FilesMatch "incl\."> Order allow,deny Deny from all </FilesMatch>
Notice that incl.
appears at the beginning of the
name. If the common file were called file.incl.php
and access were
prohibited by a similar FilesMatch
directive, then Apache when
looking for file
would write into the error log that access to
this file was rejected due to configuration settings.
Now we explain the case when we wish to insert two large parts of the HTML code. We
create two separate files file1.cs.html
and file2.cs.html
for the Czech
version, similarly file1.en.html
and file2.en.html
for the English
version. If we put virtual('file1');
and virtual('file2');
to proper
places, Apache inserts correct files by means of content negotiation. Unfortunately, this does not
always work. Remember that the correct language variant could not be found when searching for URL
called file
and the user selected the language version explicitely. Apache will react
by the very same way to the request for virtual('file1');
and offer the list of
useless links. The file for each language should define the string constant called
_ext_
containing the correct extension and the HTML code will then be inserted by
virtual('file1' . _ext_);
.
Content negotiation in PHP scripts is made use of in the MemoDisx program. You can try its demo version. You can also download the MemoDisx distribution and look into the source codes how it is done.MemoDisx makes use of frames and is entered via the main page with this contents:
<!--#include virtual="html-head.html" --> <!--#if expr="$DOCUMENT_URI != /\/index$/" --> <body> <p class="error">Please add "English" to the language preferences of your browser and then <a target="_top" href="./">try again</a>, otherwise MemoDisx will not work properly.</p> <p>More details can be found in the description of <a href="http://hroch486.icpf.cas.cz/wagner/content-negotiation">Content Negotiation</a>.</p> </body> <!--#else --> <frameset cols="15%,*"> <!--#include virtual="noframes" --> <frame name="left" src="menu"> <frame name="right" src="main"> </frameset> <!--#endif --> </html>
Variable $DOCUMENT_URI
contains the requested document name. If the name
does not contain any extension, it means that the language version was selected according to
preferences set in the browser. If the name contains an extension, we know that the user selected
the language explicitely. In such a case we would have to program everything more complex. Anyway,
MemoDisx is an application intended for repeated use. We can thus afford to ask the user to spend
more time with configuring her WWW browser.
This document is not a programmer's toolkit which you can just take and use directly. The ami of the document is to explain the method. You can, of course, extract the code examples, put them to files and get working solution. You will have to replace character entities with corresponding characters. In order not to do it by hand, you can take advantage of e simple perl script (but first you have to edit it manually):
#!perl while (<STDIN>) { s/&/&/g; s/</</g; s/>/>/g; print; }
It is supposed that global variables are registered. It is thus possible to use
$HTTP_ACCEPT_LANGUAGE
and $REQUEST_URI
. If globals were not registered,
it would be necessary to use $_SERVER['HTTP_ACCEPT_LANGUAGE']
and
$_SERVER['REQUEST_URI']
.
All links should point to the main file filename.php
which chooses the
correct language variant according to the rquest received from the WWW browser. The names of files
with the language variants will not have several extensions. Their form will be
filename-XXX_YYY.EXT
where XXX
and YYY
specify the language
and encoding, EXT
is an extension. In the examples shown here the extension can be
either php
or html
. The structure of the main file is simple:
<?php require 'engine.php'; if ($inc) { include $fn; } else virtual($fn); exit; ?>
The action is executed in file engine.php
. It returns the name of the
file containing the requested language in variable $fn
. If $inc
is set,
the file is a PHP script which should be processed by the include
command, otherwise
we use the virtual
function.
File engine.php
contains a code which is directly executed. It makes use
of functions defined in file functions.php
, therefore that file must first be included
by the require
command. None of these files must be run directly, therefore we put the
following lines to the beginning of functions.php
:
$bsn = basename($REQUEST_URI); if ($bsn == 'engine.php' || $bsn == 'functions.php') die('Access not allowed!');
The program code in engine.php
first finds the list of files. Their
names are matched to the regular expression generated by the script.
# Find the list of the files, $re evaluated from $REQUEST_URI $rq = $REQUEST_URI; if (substr($rq, -1) == '/') $rq .= 'index.php'; $re = '/^' . substr(basename($rq), 0, -4) . '-(.*)\./'; unset($flist); $dirhandle = opendir('.'); while ($fn = readdir($dirhandle)) { if (preg_match($re, $fn, $m)) { $f = array('fn' => $fn, 'ext' => explode('_', $m[1])); $flist[] = $f; } } closedir($dirhandle); if ($flist) sort($flist);
Now we find the list of acceptable languages. If the browser does not supply the list of languages we use our default similarly as Apache directive LanguagePriority.
# Get the list of acceptable languages unset($acclang); if ($HTTP_ACCEPT_LANGUAGE) { $acclang = explode(',', $HTTP_ACCEPT_LANGUAGE); for ($i = 0; $i < count($acclang); $i++) { $L = explode(';', $acclang[$i]); $acclang[$i] = trim($L[0]); } } else $acclang = array('en', 'cs');
The user can request specific language variant (e.g. en-zw) which is not available on the server. Thus if no language variant is found, we remove specifications of the variants and search again.
$xlang = FindLanguage($flist, $acclang); # Remove language variants and find a file if ($xlang < 0) { for ($i = 0; $i < count($acclang); $i++) { $L = explode('-', $acclang[$i]); $acclang[$i] = trim($L[0]); } $xlang = FindLanguage($flist, $acclang); }
The FindLanguage
function is defined in file functions.php
.
We must, however, first define the list of known languages and list of encodings (e.g. the Czech
language requires ISO-8859-2).
$languages = array( 'cs' => 'CS', 'en' => 'EN', 'fr' => 'FR', 'de' => 'DE' ); $encodings = array( 'iso2' => 'iso-8859-2' ); # Function for finding a language function FindLanguage($flist, $acclang) { $lx = -1; for ($j = 0; $lx < 0 && $j < count($acclang); $j++) { for ($i = 0; $lx < 0 && $i < count($flist); $i++) { $ext = $flist[$i]['ext']; for ($k = 0; $lx < 0 && $k < count($ext); $k++) { if ($acclang[$j] == $ext[$k]) $lx = $i; } } } return $lx; }
If no acceptable variant is found, we display the list and exit the script.
# If no file found, display variants and exit, otherwise fill the file name if ($xlang < 0) { ?> <html><body> <p>No acceptable variant was found. Your browser is set to require text in languages: <code><?php echo $HTTP_ACCEPT_LANGUAGE; ?></code> but only the following variants are available:</p> <ul> <?php for ($i = 0; $i < count($flist); $i++) { $ext = $flist[$i]['ext']; $f = $flist[$i]['fn']; reset($languages); while (list($k, $v) = each($languages)) { for ($j = 0; $f && $j < count($ext); $j++) { if ($ext[$j] == $k) { echo "<li><a href=\"$f\">$v</a></li>\n"; $f = false; } } } } ?> </ul> <p>You can read more about <a title="Content negotiation" target="content_neg" href="http://hroch486.icpf.cas.cz/wagner/content-negotiation.shtml.en">content negotiation</a> if you wish to know how to setup your WWW browser correctly.</p> <hr> </body></html> <?php exit; }
Finally we assign values to variables and optionally ask Apache for sending some response header.
$fn = $flist[$xlang]['fn']; EmitHeader($fn); $inc = preg_match('/\.php$/', $fn);
The EmitHeader
function is also defined in file
functions.php
. Some language wariants specify required character encoding. It is then
necessary to send a response header with corresponding information. File functions.php
assigns such information to extensions.
$headers = array( 'iso2' => 'Content-Type: text/html; charset=iso-8859-2' ); # Function for emitting a header function EmitHeader($fn) { global $headers; if (preg_match('/-([^-]+)\./', $fn, $m)) { $e = explode('_', $m[1]); while (list($k, $v) = each($e)) { if ($headers[$v]) header($headers[$v]); } }}
Dynamically generated pages are not usually cached. As a matter of fact, our pages are static, we can therefore ask Apache to send information about expiration.
# Longer time units define(HOUR, 3600); define(DAY, 24 * HOUR); define(MONTH, 30 * DAY); # Expiration function Expires($after = DAY) { setlocale(LC_ALL, 'EN_US'); Header('Expires: ' . gmdate('D, d M Y H:i:s', time() + $after) . ' GMT'); }
The LangLinks
function prepares links to other language variants.
# Links to languages function LangLinks() { global $fn, $REQUEST_URI, $languages; echo '<div id="langlinks"> '; $rq = $REQUEST_URI; if (substr($rq, -1) == '/') $rq = index.php; else $rq = basename($rq); if (!$fn) $fn = $rq; if (preg_match('/^([^-]+)-[^-]+$/', $fn, $m)) $rq = $m[1]; $re = '/^' . $rq . '-(.*)\./'; $dirhandle = opendir('.'); while ($f = readdir($dirhandle)) { if (preg_match($re, $f, $m)) { if ($f != $fn) { $ext = explode('_', $m[1]); while (list($k, $v) = each($ext)) { if ($languages[$v]) { $lang = $languages[$v]; printf('<a title="%s" href="%s">%s</a> ', $lang, $f, $lang); } } } } } closedir($dirhandle); echo "</div>\n"; }
The links are written close to the end of the page but displayed on top of the page via a cascaded stylesheet so that indexing robots do not index the page by language names or abbreviations. The cascaded stylesheet contains:
h1 { font-size: 180%; margin-top: 2.5em; font-family: "Tms Rmn", "Times New Roman", "Times Roman", serif } #langlinks { position: absolute; top:0.5em; right:0; background-color: rgb(153,0,51); color: rgb(204,221,255); width: 100%; text-align: right; font-family: "Lucida Sans", Helvetica, Arial, sans-serif; font-weight: bold; font-size: 125%; padding-color: rgb(153,0,51); margin-color: rgb(153,0,51); }
If the files containing the language variants are PHP scripts, their structure will be:
<?php if (!function_exists('FindLanguage')) { require 'functions.php'; EmitHeader(basename($REQUEST_URI)); } Expires(); ?> <html> <head> ... <link rel="STYLESHEET" type="text/css" href="style.css" /> </head> <body> ... <?php LangLinks(); ?> </body> </html>
Such scripts work similarly as the standard method of content negotiation.