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: # by default, non-fatal errors are not 'promoted': 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 # $(XMAKE_PROJECT_DIR)/XMakefile.mks # XME_explicitRule USAGE: # $(eval $(call # XME_explicitRule,$(ext),$(srcfile),$(prerequisites),$(cmdOptions))) # '$' must be escaped by '$$$$' if used in a variable php_code:=$$$$user="greg"; $(eval $(call XME_explicitRule,phpc,$(CURDIR)/custom1.php.phpc,,$(php_code))) # '$' must be escaped by '$$' if passed directly $(eval $(call XME_explicitRule,phpc,$(CURDIR)/custom2.php.phpc,,$$user="greg";)) # EOF 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 # Makefile example # create localized output from a PHP script; pass runtime parameters 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="$*";,$@) # EOF

.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).