XMake PHP
XMake version: 0.93a
Release Date: 08/17/05
Copyright (c) 2003 Gregory Keranen. All rights reserved.
Contents
Introduction
Installation
Usage
Configuration
PHP Command Line Environment
XMake/phpc Execution Environment
Simulated Web Server Environment
Error Handling & Quality Control
File Dependencies
Static & Dynamic Templating
Makefile Customization
.phpc Source File Security
PHP Bugs: Exit Status of the PHP Command
Introduction
An introductory slide show from International PHP Conference 2003, in
Amsterdam is bundled with the xmake distribution. This is also an
excellent example of how to use XMake with PHP:
phpconf2003/slides/index.html
Installation
The 'phpc' XMExtension, requires the command-line version of PHP ("PHP CLI
SAPI", or "PHP CLI").
PHP versions >=4.3.2 are recommended, although earlier versions will work.
To install PHP, compile PHP sources and run;
make install-cli
This will copy the command-line binary, sapi/cli/php, to
/usr/local/bin/php, unless you configured the option ./configure
--prefix=/some/other/path
Alternatively will be installed as /usr/local/bin/php, if you configure in
one of the following ways:
./configure --with--apxs ...
or
./configure --disable-cgi ...
then:
make install
See: http://www.php.net/manual/en/features.commandline.php
Relevant points, excerpted from the above link:
As of PHP 4.3.0, the name, location and existence of the CLI/CGI binaries
will differ depending on how PHP is installed on your system. By default
when executing make, both the CGI and CLI are built and placed as
sapi/cgi/php and sapi/cli/php respectively, in your php source directory.
You will note that both are named php. What happens during make install
depends on your configure line. If a module SAPI is chosen during
configure, such as apxs, or the --disable-cgi option is used, the CLI is
copied to {PREFIX}/bin/php during make install otherwise the CGI is placed
there. So, for example, if --with--apxs is in your configure line then the
CLI is copied to {PREFIX}/bin/php during make install. If you want to
override the installation of the CGI binary, use make install-cli after
make install. Alternatively you can specify --disable-cgi in your
configure line.
Usage
To Use XMake with PHP:
+ Configure XMake: enable 'phpc' XMExtension
+ Install PHP CLI SAPI (see above)
+ Create a new project with 'xmakep' command
+ Create some source files with .phpc suffix
+ Process all source files with 'xmake' command
+ Optional: create custom makefile rules
XMake does not ordinarily process .php files; only .phpc files. This naming
convention distinguishes environmental requirements: .php files may require
a web server environment whereas .phpc files do not.
Configuration
phpc XMExtension provides configurable options which can be set in the
project config file, XMake.conf, or, globally, in
../config/XMExtensions/phpc.conf
If set globally, settings will be copied to all XMake.conf files created
with 'xmakep' command.
PHP Command Line Environment
There are several important differences in how CLI SAPI behaves compared to
other PHP binaries.
The PHP manual describes these differences in detail:
http://www.php.net/manual/en/features.commandline.php
NOTE: The CGI version, used from the command line, is NOT a substitute
since it does not allow '-r' option, used by phpc pattern rules
"SAPI" (Server Application Programming Interface) is somewhat misleading
when applied to the command line PHP binary, since command line PHP
scripts execute WITHOUT any server involved at all.
Scripts which assume a web server environment probably will not work
correctly with CLI SAPI: environmental variables, such as $DOCUMENT_ROOT,
$REQUEST_URI (and $_SERVER["DOCUMENT_ROOT"], $_SERVER["REQUEST_URI"]) will
not be defined.
When processing .phpc files, the following conditions apply:
+ XMake always changes directory to that of a .phpc file before
invoking PHP
+ Any .phpc script can easily include files in the same directory, with
only a filename as relative path. This makes up for the fact that PHP CLI
does not change the current directory to the directory of the executed
script. Your script can reliably determine the directory it is executing
from with getcwd().
XMake/phpc Execution Environment
phpc XMExtension uses makefile macros to execute .phpc files (or any PHP
files you want) within an enhanced command line PHP environment.
Full details of how this works are contained in the source files:
+ ../config/XMExtensions/phpc.mkh
+ ../config/XMExtensions/phpc.mkr
+ ../config/XMExtensions/phpc.inc
.phpc files are processed by XMake by including them within a command line
of the (simplified) form:
/path/to/php \
-c ${XMAKE_EXT_PHPC_PHP_INI} \
-d log_errors=1 \
-d error_log= \
-d display_errors=0 \
-d display_startup_errors=0 \
-r '... \
include(getenv("XMAKE_HOME")."/config/XMExtensions/phpc.inc"); ... \
$XMAKE_XME_PHPC_ERROR_REPORTING_MASK =
getenv("XMAKE_XME_PHPC_ERROR_REPORTING_MASK"); \
error_reporting($XMAKE_XME_PHPC_ERROR_REPORTING_MASK); \
/* set some PHP globals */ \
set_error_handler("phpc_error_handler"); \
/* execute any PHP code passed in: */ \
include("/path/to/myFile.phpc"); \
...' \
1> "/path/to/myFile" ...
Important functions for error handling, file dependency detection and other
utility functions are included in the file
${XMAKE_HOME}/config/XMExtensions/phpc.inc. XMake.conf options can be used
to simulate a web server environment (see discussion below).
phpc.inc sets several PHP globals variables to the full path of the current
script, resolving any symbolic links to the current directory. The affected
variables are:
$_SERVER["PHP_SELF"]
$_SERVER["SCRIPT_NAME"]
$_SERVER["SCRIPT_FILENAME"]
$_SERVER["PATH_TRANSLATED"]
$HTTP_SERVER_VARS["PHP_SELF"]
$HTTP_SERVER_VARS["SCRIPT_NAME"]
$HTTP_SERVER_VARS["SCRIPT_FILENAME"]
$HTTP_SERVER_VARS["PATH_TRANSLATED"]
$_SERVER["SCRIPT_FILENAME"] and $_SERVER["PATH_TRANSLATED"] will have the
same values in PHP CLI context as they would have running in a web server
context. $_SERVER["PHP_SELF"] and $_SERVER["SCRIPT_NAME"] are not the
same, since these would be relative to $_SERVER["DOCUMENT_ROOT"].
Simulated Web Server Environment
phpc.inc uses the XMake.conf variable XMAKE_EXT_PHPC_GLOBAL_INC to allow
you to easily simulate a web server environment in command line phpc
scripts.
The absence of $DOCUMENT_ROOT in the command line context is of special
concern since it is often useful for .phpc scripts to create valid links
to other files that will be referenced at run time.
In the project XMake.conf file, set the variable XMAKE_EXT_PHPC_GLOBAL_INC:
export XMAKE_EXT_PHPC_GLOBAL_INC=${XMAKE_PROJECT_DIR}/phpc.global.inc
This file will be automatically included by XMake, prior to including any
.phpc source files.
Now create the file 'phpc.global.inc' in your project directory to set any
global variables you need; or copy and edit the included sample
../config/XMExtensions/phpc.global.inc
Now your scripts can create static links that will work in any web server
environment.
Simply set $DOCUMENT_ROOT in the XMake project equal to the same value as
$DOCUMENT_ROOT that applies to the project and reference it from your
.phpc file, for example:
<?php echo '<a
href="'.$_SERVER['DOCUMENT_ROOT'].'relative/path/to/file.html">file.html</a>';
?>
EXAMPLE:
If you use Apache and have several virtual servers configured below the
apache/htdocs/ directory, generally you should create a separate XMake
project in the DocumentRoot directory corresponding to each VirtualHost
directive and create a 'phpc.global.inc' file in each XMake project,
setting XMAKE_DOCUMENT_ROOT=[VirtualHost DocumentRoot value]
Error Handling & Quality Control
One of the greatest weaknesses of PHP, and web applications in general, is
in the area of quality control. Although excellent debugging tools, such
as DBG, Zend IDE, are available, these are designed for manual debugging
of individual scripts rather than routine, automated batch processing of
an entire site.
XMake supports automated testing of fatal errors in .phpc files, as part of
the routine build process, as well as lint checking for .phpc and .php
files. When you process a .phpc file with XMake, you simultaneously test
it: the build process succeeds or fails, based on the exit status of each
source file. If fatal errors are encountered, no output file is written.
XMake gives you complete control over what level of error codes trigger
fatal exit status and, hence, the overall quality of your code is
maintained at whatever level you desire.
First, some recommended coding conventions: always use trigger_error(...)
instead of echo(...); exit(1); for error messages. XMake actually works
around this and displays STDOUT on STDERR, but only if a fatal error is
generated.
POOR:
<?php
// echo always goes to STDOUT
echo("file doesn't exist\n");
exit(1);
?>
PREFERRED:
<?php
// XMake displays on STDERR and exits, at build time
trigger_error("file doesn't exist",E_USER_ERROR );
?>
The phpc XMExtension attempts to achieve a clean separation of error
messages from normal script output by always setting these php.ini options
when invoking PHP CLI:
/path/to/php -c /path/to/php.ini \
-d log_errors=1 \
-d error_log= \
-d display_errors=0 \
-d display_startup_errors=0
log_errors=1
- sends errors to error_log, or /dev/stderr
error_log=
- empty value causes error_log() to use /dev/stderr; inhibits prepending of
date to error message for display purposes, as would be the case, if
error_log="/dev/stderr" was used.
display_errors=0
- don't want errors on STDOUT
display_startup_errors=0
- we get all errors on STDERR anyway with logging
The desired data flow is:
+-> stdout -> myFile
| no
myFile.phpc -> [xmake-PHP-xmake] --> [error?]
| yes
+-> stderr -> console
We say 'desired' data flow because PHP has some bugs and limitations:
+ BUG: default error handler sometimes writes to STDOUT instead of
STDERR
XMake tries to work around this potential issue by dumping STDOUT
to STDERR
However, this only works for FATAL errors
+ BUG: fatal errors may not always exit with correct status
This happened with require(), until recently; there may be other
bugs lurking
XMake uses a workaround, for backward compatibility
+ system errors (E_ERROR, E_CORE_WARNING, etc.) CANNOT be handled with
custom error handler; non-fatal errors such as E_CORE_WARNING may occur
during startup (prior to installation of custom error handler)
You cannot control the fatality of such errors but they should all
go to STDERR
They key to automated testing is controlling the exit status of scripts.
While PHP scripts can control exit status with exit(), it is more
effectively managed using a custom error handler.
PHP's set_error_handler() function is the primary tool which XMake uses to
handle script errors. XMake is bundled with a custom PHP error handler,
phpc_error_handler(), which is installed by default before .phpc scripts
are parsed. It supports these features:
control over which error codes cause 'FATAL' (non-zero) exit status
correct routing of all error messages to /dev/stderr
custom formatting of error message output
phpc_error_handler() and other functions for error handling and file
dependency detection are included in: xmake/config/XMExtensions/phpc.inc
PHP's default error handler does not allow you to change the level at which
errors cause non-zero (fatal) exit status. Changing the error reporting
level does not affect exit status; it only affect the level at which error
messages are displayed or logged.
The default PHP error handler sets the fatal error level to:
E_ERROR|E_PARSE|E_CORE_ERROR|E_COMPILE_ERROR|E_USER_ERROR
= 1|4|16|64|256 = 341
Warnings, such as E_WARNING are not fatal and will exit normally, with the
default error handler. This approach assumes that individual scripts must
contain logic to test for non-fatal errors and to 'promote' the error
level to E_USER_ERROR, if they should be fatal with something like the
following:
<?php
// failure of readfile("non_existent_file") is not fatal;
// E_WARNING error is generated
if (!file_exists($file)){
trigger_error("file doesn't exist\n",E_USER_ERROR );
} else {
readfile($file);
}
?>
There are a few of problems with the above approach:
Legacy code may not have robust error handling; no custom handler
New code may contain bugs; warnings that should be fatal
You may want to control your code more selectively, without modifying
sources
XMake allows user-control over the fatal error level by means of a global
variable, XMAKE_XME_PHPC_FATAL_ERROR_MASK, in the project configuration
file, XMake.conf.
By default, phpc_error_handler() exits with any error except NOTICE level:
XMAKE_XME_PHPC_FATAL_ERROR_MASK=770
E_WARNING|E_USER_ERROR|E_USER_WARNING=2|256|512=770
The following errors are always handled by PHP internally, and cannot be
handled with any handler installed with set_error_handler():
E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING, E_ERROR, E_PARSE,
E_CORE_ERROR
If another custom error handler function is installed, from within any
script, it will override the phpc_error_handler() function, installed by
XMake. It is recommended that any custom error handlers follow the example
of phpc_error_handler() to always Do The Right Thing, by supporting control
of fatal error levels and proper redirection of error messages to STDERR.
Lint Checking
The built-in PHP CLI 'lint' option, 'php -l ...', is almost useless since
it only detects E_PARSE errors, which are fatal and, therefore, always
caught by XMake anyway.
PHP lint checking is built in to XMake, for both *.php and *.phpc files,
using targets: 'php.lint' and 'phpc.lint'. Fatal error levels are
configured in XMake.conf:
XMAKE_XME_PHPC_LINT_FATAL_ERROR_MASK=0
WARNING:
Lint checking EXECUTES all *.php or *.phpc scripts in the XMake project
tree.
If you have a script lying around that could do bad things like <?php
system("rm -rf /;");?> then it most likely WILL do those things if it
resides within an XMake project and you execute 'xmake php.lint' or 'xmake
phpc.lint'. Of course, this depends on the file permissions in effect.
File Dependencies
The most important requirement in writing .phpc files is to enable XMake to
know about all file dependencies: any file used by a PHP script could
affect the output and is considered a prerequisite file - any changes to
prerequisites cause Make to rebuild the output file.
In the case of included and required files, XMake will determine these
automatically by calling the built in function, get_included_files(). In
all other cases, when a script accesses a file with file(), readfile(),
etc., there is no way for XMake to know of these dependencies unless the
programmer includes logic to register such files.
XMake provides a function which handles registration of file dependencies:
void phpc_add_prerequisites( mixed $prerequisiteFiles )
phpc_add_prerequisites() accepts a single filename string or array of
filenames as input.
EXAMPLE:
example1.phpc
<?php
// suppose you want to use 'file1' in the same directory as this script:
$prerequisite_file='file1';
phpc_add_prerequisites( $prerequisite_file );
readfile($prerequisite_file);
?>
Perhaps the trickiest aspecs of using PHP with XMake is:
1. Determining what the prerequisite files are
2. Remembering to call phpc_add_prerequisites($file) when your script uses
a $file
3. Make sure phpc_add_prerequisites() is called for all files used, IN ALL
POSSIBLE CODE PATHS. Dependencies are determined by XMake simply by
executing the source file, without knowledge of any particular modes of
use: parameters, global vars, etc. that might affect what code gets
executed in building any particular targets.
The points to keep in mind are:
1. All files which affect script behavior, other than those inside
include() or require() statements, must be passed to
phpc_add_prerequisites().
2. You can call phpc_add_prerequisites() at any point, before or after
using the registered files since XMake disgards all input when determining
prerequisites: dependency makefiles are made when the makefile is parsed,
PRIOR to building all other targets, such as output files.
3. To force XMake to always rebuild a file, use: phpc_add_prerequisites(
"FORCE" );
4. If you use remote files, use: phpc_add_prerequisites( "FORCE" );
Sometimes determining prerequisites is obvious - as in the above example
when readfile() is called to output the contents of a file - but sometime
it is not.
Consider the following case:
example2.phpc
<?php
if (file_exists("volatile_file")) echo("file exists");
?>
What is the prerequisite of example2.phpc? "volatile_file"?
No. It is the directory containing "volatile_file" since it will change if
"volatile_file" is created or deleted; if "volatile_file" was registered
as a prerequisite, then example2 would not be re-made if "volatile_file"
were deleted.
The correct usage is:
example2.phpc
<?php
if (file_exists("volatile_file")) echo("file exists");
phpc_add_prerequisites( getcwd() );
?>
Consider another example:
example3.phpc
<?php
echo(disk_free_space ( getcwd()));
?>
What is the the prerequisite file here? The current directory?
NO! In this case, all files below the current directory, including all
directories, could be registered as prerequisite files, but that would be
very cumbersome to code. What we really want is to FORCE XMake to always
re-make the output file related to this script.
This is accomplished with the special PHONY makefile target: 'FORCE'
(case-sensitive).
Since Make always considers PHONY prerequisites out of date, adding "FORCE"
as a prerequisite has the desired effect of making the output file always
appear out of date.
The correct usage is:
example3.phpc
<?php
echo(disk_free_space ( getcwd()));
phpc_add_prerequisites( "FORCE" );
?>
The above example can apply to any dynamic script:
example4A.phpc
<?php
echo("the time is:".date("D M j G:i:s T Y"));
phpc_add_prerequisites( "FORCE" );
?>
But you wouldn't use it in this case:
example4B.phpc
<?php
// no prerequisites
echo("Hello World");
echo("This file was last updated on:".date("D M j G:i:s T Y"));
?>
Another case for using "FORCE", is when accessing remote files:
example5.phpc
<?php
$html = implode ('', file ('http://www.example.com/ '));
echo $html;
phpc_add_prerequisites( "FORCE" );
?>
Gnu Make cannot read the file modification times of http or ftp resources
such as, http://www.example.com/ ; it only handles files on locally
mounted filesystems.
Static & Dynamic Templating
To create dynamic .php files from .phpc files, it is necessary to pass
run-time code through as text - i.e., outside of any <?php ... ?>
processing instruction tags.
This can be accomplished in two ways:
1. using readfile(), instead of include() or require(), to output code
from another file
2. embed dynamic code directly, as plain text
Case 1: using readfile():
<?php
// register prerequiste file:
$prerequisite_file="dynamic_code.php.inc";
phpc_add_prerequisites( $prerequisite_file );
readfile($prerequisite_file);
?>
Case 2: embedded, using phpc constants:
<?= PHP_OPEN_TAG;?>
// code to execute at run time
<?= PHP_CLOSE_TAG;?>
Makefile Customization
The default makefile pattern rule used for processing .phpc source files
applies to all .phpc, unless you write custom rules. Because the default
rule is a generic rule, it does not pass any custom options or variable
settings to the source files.
In order to make the most of XMake, we can write custom rules to allow
cusom PHP code to be evaluated in global scope prior to evaluating a
source file.
Make requires escaping '$', which is a little ugly but worth the price:
this technique allow you to pass arbitrary code, not just command line
arguments.
Another advantage is that you can pass arrays from XMake to PHP, without
multiple assignments ('php -f myfile.php a[]=a a[]=b a[]=c') or
serialization, for example, as would be required otherwise.
EXAMPLE: creating explicit rules for .phpc files
php_code:=$$$$user="greg";
$(eval $(call
XME_explicitRule,phpc,$(CURDIR)/custom1.php.phpc,,$(php_code)))
$(eval $(call
XME_explicitRule,phpc,$(CURDIR)/custom2.php.phpc,,$$user="greg";))
XMake is not limited to creating output from .phpc files with a one-to-one
source->output file mapping.
You can use phpc makefile macros to provide a one-to-many source->output
file relationship with .php source files, for example.
EXAMPLE: using phpc macros to create rules for .php files
LOCALIZED_OUTFILES:=$(CURDIR)/multi-lingual.en $(CURDIR)/multi-lingual.fr
$(LOCALIZED_OUTFILES): $(CURDIR)/multi-lingual.%
:$(CURDIR)/multi-lingual.php
@$(call
XME_phpc_outfileMacro,$(CURDIR)/multi-lingual.php,$$lang="$*";,$@)
.phpc Source File Security
Two choices:
1. Don't upload these files to a public server. Instead, create an 'upload'
target that excludes all *.phpc files
2. Configure Apache with the following .htaccess options:
This method is preferred since it is fastest: .phpc files are parsed like
.php files:
#for PHP3
AddType application/x-httpd-php3 .phpc
#for PHP4
AddType application/x-httpd-php .phpc
OR: use <Files ...> directive to give the user a "permission denied" error
when any .phpc file is requested.
Slower than the AddType method since every requested file must be matched
against the pattern.
<Files ~ ".phpc">
Order allow,deny
Deny from all
</Files>
PHP Bugs: Exit Status of the PHP Command
For XMake to work correctly, it depends on receiving the correct exit
status from commands used by XMExtensions:
when a command fails, it must return non-zero exit status to the shell. For
phpc XMExtension to work correctly, PHP must exit with non-zero status when
fatal errors occur.
PHP versions prior to 4.3.2-RC of Mar 19 2003 exhibit a bug and FAIL to
exit properly. Bug #22775: Fatal error from require() exits with status=0.
See: http://bugs.php.net/bug.php?id=22775
Therefore, either use PHP >= 4.3.2, or use the following workaround in your
scripts, for prior versions. The only workaround is to never use require().
${XMAKE_HOME}/config/XMExtensions/phpc.inc contains a workaround function,
phpc_require_once(), which uses include(), calling exit(1), if the file
doesn't exist.
All bundled XMake PHP scripts use the above workaround and will work
correctly with PHP versions >= 4.3.0 (and possibly earlier).