Archive for November, 2007
Refine and debug PHP applications with syslog
PHP version of venerable UNIX syslog offers a simple, effective debugging tool
An old technique for exploring a running program is to place code that “displays” the current value of variables at strategic points. But how is this done without interfering with the standard output of the program? With PHP’s
syslog()facility, examining these values is easy. Find out how.
<!– if (document.referrer&&document.referrer!=”") { // document.write(document.referrer); var q = document.referrer; var engine = q; var isG = engine.search(/google\.com/i); var searchTerms; //var searchTermsForDisplay; if (isG != -1) { var i = q.search(/q=/); var q2 = q.substring(i+2); var j = q2.search(/&/); j = (j == -1)?q2.length:j; searchTerms = q.substring(i+2,i+2+j); if (searchTerms.length != 0) { searchQuery(searchTerms); document.write(“
“); } } } //–> Programming a computer is a tedious business, but it’s fun, too. One of the fun aspects of programming is learning about new ways to use an old tool. Recently, I was contracted to fix a dozen bugs in a large, complex, Linux®, Apache, MySQL, and Linux, Apache, MySQL, PHP/Perl (LAMP)-based content-management system (CMS). The architecture of the CMS was the standard LAMP model, with Enterprise Red Hat Linux running Apache V2.0. The code that drove the Web sites consisted of a few hundred PHP source modules spread out over 30 subdirectories in the Apache document root directory. The Apache and MySQL portions of the system required no changes, so all my bug-busting activities were to occur in the PHP workspace.
After spending considerable time learning how the CMS worked, I grew to appreciate the system’s elegant design and I realized that, as in most mature programming environments, this system relied on only a small number of the available PHP functions. (Shades of the old 80/20 rule here, where 80 percent of the work is accomplished with 20 percent of the available functions.) This article shows how the process of debugging an unknown but complex system can help you learn about those little-used functions and provides examples of how to apply your new knowledge by using the rich functionality of the syslog() function.
With literally hundreds of functions available in the PHP programming language, some (dare I say most?) functions are never used unless you read about them in articles like this one. Another way to learn more about the language is to debug programs that others have written in it. I’m always impressed by the creative ways in which programmers use the tools.
Debugging, like computer programming, is part science and part art. When tracking down obscure bugs in a system that you didn’t create, you need to have a good feel for where they manifest themselves in the code. With hundreds of code modules to deal with, a good place to begin is where the output revealing the bug is found, then work your way backward from there to isolate the problem.
Say, for example, that a calculated value that was being output is known to be incorrect. You must devise some way to see the intermediary values that lead up to the problem. Another problem may manifest itself with bad data coming from an important database query. You need to be able to see the SQL statement generated (and submitted by the code to the MySQL engine) to see whether that’s where the problem lies.
One age-old technique for doing this is to insert code that simply prints these strings and values. Unfortunately, with a tool like PHP, or any Web-based application, you don’t want to clutter up the natural output of systems like this (that is, HTML code being sent to the browser) with debugging information, especially if the system you’re examining is a production server.
You could create a custom logging file and write log messages to it, but why not use the tools that have already been provided for you? Always remember: Don’t bury your tools. The time spent seeing whether the language authors have already provided a library routine or function to do something you suspect would be a commonly needed function is well spent. In fact, anytime you suspect that something should obviously be part of a programming package, it probably is.
I realized that what I needed to use was something similar to the syslog functionality of UNIX®. I thought it likely that PHP would have a hook into the syslog functionality, so upon a quick review of the PHP documentation, I found it! The PHP syslog() function is one that I had overlooked for many years until I was contracted to do this project. With syslog(), I was able to hunt down and exterminate most of the bugs in the production system without introducing side effects other than the minute amount of time it took for each function to execute.
As you grow in your knowledge and become more skilled in your craft, different ways of doing things become apparent, and some way will eventually become the obvious way to perform the required task. What you must always keep in mind is that what is obvious to you is not obvious to other people. What may be simple to you can present a mountain of complexity to others.
Using a tool like the syslog() function keeps complexity under control and provides the added benefit of allowing easy customizations through configuration files, such as syslog.conf. For example, you can insert code to format a SQL statement, log it to syslog(), and pass it to MySQL for execution. Later, a slight modification of the syslog.conf file sends the text to a different log file or perhaps just to the bit bucket. Weeks or months later, if it becomes necessary to see what’s happening again, another simple change to the syslog.conf file will restore the functionality.
|
syslog has a rich and colorful history in the UNIX world. Originally developed as part of the Sendmail project, syslog had proven to be so useful that many other tools have incorporated in its functionality, proving that the simplest ideas are sometimes the most powerful.
In a nutshell, syslog allows applications to write tagged messages to a common set of system log files that can reside where it’s convenient for the programmers and network administrators to access. This means, for example, you can configure syslog on a Web server to log system messages on a different server — one that is perhaps a few layers deeper behind the security firewalls and easier for the aforementioned administrators to access. But for my purposes, I just accept the default behavior of files being written to the /var/log directory.
The syslog mechanism is started at boot-up, and its initial behavior is defined by the rules in the syslog.conf file. These rules enable you to finely tune what can and cannot be logged with the mechanism — which, on a high-volume, hard-working server can translate to log files of a manageable size.
Each rule consists of two fields: selector and action. Basically, the selector field selects what facility will be logged (for example, kern, user, mail, lpr) and what priority the facility has. The priority field contains a keyword, such as debug, info, notice, or warning. And the action field of a rule defines what to do with messages of a kind that match the selector field. The rules can specify which file to log the message to, what machine to send the logging message to, what user to send the message to (in the form of console messages), etc.
|
Study the info and man pages associated with the syslog facility to learn how to configure the system for your needs. In my case, I needed to know what the value of various PHP variables were at different times while the production CMS system was running. I also needed to know when certain modules were started and ended, as well as what various intermediate variable values were. Before getting into specifics about what to log, let’s set up the basics for logging.
With your favorite editor, create the file shown in Listing 1, name it test.php and put it where your Apache document root is found (on my system, it’s /var/www).
Listing 1. test.php
<html>
<head>
<title>PHP Test Page</title>
</head>
<body>
<?php
syslog(LOG_NOTICE, "{$_SERVER['REMOTE_ADDR']}: test.php - PHP Index page accessed.");
echo '<p>PHP Test Page</p>';
?>
</body>
</html>
|
You should have PHP installed and configured, as well. If not, check Resources for links on how to do this. If you configured everything properly, you should see the following text in your browser when you access the page with the URL http://localhost/test.php:
PHP Test Page |
Next, open an X terminal window and type the following command to see what was logged to the /var/log/messages file:
tail /var/log/messages |
If all goes well, you should see a line like this one near the end of the listing:
Jul 23 14:43:42 localhost apache2: 127.0.0.1: test.php - PHP Index page accessed. |
If you did, that’s great. You have now verified that all you need to do to begin detailed debugging of the complex system you’re working on.
I have found it a good practice to use syslog() for all the PHP/MySQL calls so that every time you interact with the Web site, you can see which SQL queries are generated in real time to build the page returned to you. A handy way to make this work is to open a spare X terminal window and use the tail command with the -f option for “live” viewing of the messages log:
tail -f /var/log/messages |
|
Now that you’ve verified that the logging works and that you can see the results of the logging mechanism, here are a few tips for shortening the process of learning a foreign system as quickly as possible using these tools.
It’s important to know what modules do what beyond what their documentation may say. For this reason, I like to include logging messages that mark the beginning and end of the modules. In this way, you can play with the pages of the Web site, keeping an eye on the spare X terminal window running the tail -f /var/log/messages command. In this way, you see which modules execute and in what order every time the browser requests a new page.
I also like to log the boundaries between different programs being called into action during the running of the PHP code. Examples include when the MySQL calls are made to query the databases or when external programs are called to make formatting changes to data (Extensible Stylesheet Language Transformation (XSLT) engines, for example). The very practice of systematically inserting these checkpoints into each PHP code module helps you get used to the names and locations of the modules. When the code sends you messages you see in the spare X terminal while browsing various pages, you get important feedback that facilitates and enhances your knowledge of the system.
|
The syslog facility is a powerful tool for debugging an application someone else wrote. It allows you to watch which modules are being executed, which SQL statements are executing, and which variable values change as you navigate the Web site. This helps pinpoint the modules in which you’re likely to find problems.
Learn
- Learn more about the
syslog()function. - Discover the history of syslog on Wikipedia.
- PHP.net is the central resource for PHP developers.
- Check out the “Recommended PHP reading list.”
- Browse all the PHP content on developerWorks.
- Expand your PHP skills by checking out IBM developerWorks’ PHP project resources.
- To listen to interesting interviews and discussions for software developers, check out developerWorks podcasts.
- Using a database with PHP? Check out the Zend Core for IBM, a seamless, out-of-the-box, easy-to-install PHP development and production environment that supports IBM DB2 V9.
- Stay current with developerWorks’ Technical events and webcasts.
- Check out upcoming conferences, trade shows, webcasts, and other Events around the world that are of interest to IBM open source developers.
- Visit the developerWorks Open source zone for extensive how-to information, tools, and project updates to help you develop with open source technologies and use them with IBM’s products.
- Watch and learn about IBM and open source technologies and product functions with the no-cost developerWorks On demand demos.
Get products and technologies
- Innovate your next open source development project with IBM trial software, available for download or on DVD.
- Download IBM product evaluation versions, and get your hands on application development tools and middleware products from DB2®, Lotus®, Rational®, Tivoli®, and WebSphere®.
Discuss
- Participate in developerWorks blogs and get involved in the developerWorks community.
- Participate in the developerWorks PHP Forum: Developing PHP applications with IBM Information Management products (DB2, IDS).
Add comment November 2, 2007
PHP Web Site Generation using Ruby
by Eric Rollins
ntroduction
“Raising the level of abstraction means moving toward WHAT, not HOW – telling the system what we want to do, declaratively,instead of how to do it, procedurally. This trend is desirable because declarative means the system does the work, while procedural means the user does the work.” C.J. Date, What Not How, 2000.
In Chapter 10 Database Access of Code Generation in Action Jack Herrington and I presented a code generator written in Ruby that generated a SQL schema and Enterprise Java Beans database access tier based on an input XML schema description. For this article I have modified the Ruby code to generate PHP code, and extended it to generate production PHP/HTML web pages.
I will cover building a database access layer for PHP, but I will also focus on the numerous benefits of generating production web pages using a system that has complete knowledge of the database schema. To finish I will discuss the highly productive development style that is enabled by continuously extending the declarative grammar in your own generator, and then provide some conclusions.
The Ruby source code and other files are available here.
I will not be explaining the Ruby source code; a detailed explanation is presented in Chapter 10 of the book.
And code generation provides many other advantages I do not discuss, please see the book and the Code Generation Network website FAQ for more.
Test Case
Our test case is an application that manages book publishing. It consists of 5 tables:
Book
| bookID | title | ISBN | authorID | publisherID | status | numCopies |
| 100 | Object Oriented Perl | 1-884777-79-1 | 100 | 100 | 2 | 1 |
| 101 | Bitter Java | 1-930110-43-X | 101 | 100 | 2 | 1 |
Author
| authorID | name | penName |
| 100 | Conway | |
| 101 | Tate |
Publisher
| publisherID | name |
| 100 | Manning |
Store
| storeID | name |
| 100 | Borders |
StoreBook
| storeID | bookID | quantity |
| 100 | 100 | 45 |
| 100 | 101 | 399 |
Generator Architecture
Our generator will take four XML files as input and using a series of templates will build a series of SQL and PHP files. The diagram below shows this relationship:

The four input files are:
| schema.xml | Describes the database tables, columns, column datatypes, andforeign key relationships. |
| extensions.xml | Describes extended Value Objects, queries, and methods beyond those automatically generated from schema.xml. |
| pages.xml | Describes production list, add, update, and delete web pages which access tables described in schema.xml. |
| samples.xml | Describes sample data that is placed in a Tests.php web page to be automatically loaded via the generated PHP APIs. |
The output files are:
| tables.sql | SQL script that creates tables and foreign key relationships. |
| code/*SS.php | Database access layer (class) for table *; provides add, update, delete, get, getAll, and custom queries and methods. |
| code/*Value.php | Value Object class for table *; used to pass data between web pages and *SS layer. |
| tests/Tests.php | PHP web page used to load sample data described in samples.xml. |
| webtest/*Add.php, *Update.php, *Delete.php, ValueList.php | Test PHP web pages automatically generated for table *. |
| web/*.php | Production PHP web pages specified in pages.xml. |
Database Access Architecture
The PHP web pages do not access the database directly. Instead they pass Value Objects to the SS layer, which in turn calls the PHP PEAR DB database abstraction layer API. This is diagrammed below:

The web browser talks to the PHP page on the Apache web server. The web page creates (or requests) a Value Object. The Value Object is passed to the SS layer, where it is transformed into a SQL statement passed to the PEAR database abstraction layer API, and on to the database.
PHP Generator
Now that we have completed a high level overview, lets drill into the generator itself. We first start with the lowest level, the schema.
Schema
The fundamental input to the generator is the schema.xml file. A fragment is show below:
<table name="Book">
<column name="bookID" datatype="integer" not-null="true"
primary-key="true" />
<column name="title" datatype="varchar" length="80" not-null="true" />
<column name="ISBN" datatype="varchar" length="80" not-null="true"
unique="true" />
<column name="authorID" datatype="integer" not-null="true" />
<column name="publisherID" datatype="integer" not-null="true" />
<column name="status" datatype="integer" not-null="true" />
<column name="numCopies" datatype="integer" not-null="true" />
</table>
<foreign-key>
<fk-table>Book</fk-table>
<fk-column>authorID</fk-column>
<fk-references>Author</fk-references>
</foreign-key>
<foreign-key>
<fk-table>Book</fk-table>
<fk-column>publisherID</fk-column>
<fk-references>Publisher</fk-references>
</foreign-key>
The schema.xml file specifies the database tables, columns, column datatypes, etc. It also specifies the foreign key relationships between tables. It is initially used to generate the database SQL schema file. The corresponding generated fragment is:
create table Book (
bookID integer not null
,title varchar(80) not null
,ISBN varchar(80) not null unique
,authorID integer not null
,publisherID integer not null
,status integer not null
,numCopies integer not null
,constraint Book_pk primary key(bookID)
);
alter table Book
add constraint Book_authorID
foreign key (authorID)
references Author (authorID);
alter table Book
add constraint Book_publisherID
foreign key (publisherID)
references Publisher (publisherID);
In this example all table columns are listed directly in schema.xml.
Stereotypes
Unlike the generated SQL file, the schema.xml file does not need to directly list all the fields, datatypes, etc. of a table. In production systems many tables contain repetitive columns for purposes of audit trail, optimistic locking, etc. Instead of manually adding these fields to each table a stereotype may be used. This concept has been borrowed from UML, where it is represented in class diagrams as <<stereotype_name>>. Here we take advantage of our extensible XML grammar to attribute tables with desired stereotypes. An example would be to mark tables as <constant/> or <dynamic/>.
Dynamic tables would automatically have columns create_date, modification_date, and modification_count, used in optimistic locking, added. Database SQL trigger code to maintain these fields can be generated. Higher-level layers of generated code would automatically utilize these locking columns transparent to the developer.
Constant tables can produce generation-time or run-time warnings if they are modified. Later the <dynamic/> semantics could be extended to track column usage by adding created_by and modified_by columns, again automatically maintained by the higher layer code and again transparent to the developer.
The leverage provided by applying arbitrarily complex semantics to simple extensions to the grammar is a key advantage of this generation system. The equivalent in a UML-based generation system would be to apply a new custom stereotype <<dynamic>> to a class and extend the generator to implement the semantics.
Stereotypes leave the application developer free to concentrate on the important elements of the data design.
Basic SS Layer and Value Objects
The SS database access layer separates web pages from PEAR database access routines. The web pages communicate with the database by passing Value Objects. The SS layer is implemented as a separate PHP class wrapping each schema table. Default methods on the class accept or return Value Objects to add, update, delete, get, and getAll database rows. Independent of any customization a basic Value Object and SS layer is generated for each table in schema.xml.
The basic Value Object looks like this:
class BookValue {
// private member variables
var $_bookID;
var $_title;
var $_ISBN;
var $_authorID;
var $_publisherID;
var $_status;
var $_numCopies;
// empty constructor
function BookValue(){
...
}
// ResultSet constructor
function setFromRow($row){
...
}
// member variable getters and setters
function getBookID() { return $this->_bookID; }
function setBookID($bookID){ $this->_bookID = $bookID; }
...
}
When you add or edit a record you need to construct or fetch a Value Object, then alter its contents and send it to the SS layer to be stored in the database.
The standard SS layer API looks like this:
class BookSS {
function getBookValue($bookID)
// returns array of BookValue
function getAllBookValue($orderBy)
function add($value)
function update($value)
function delete($bookID)
}
Extending the API
From schema.xml we create the SS layer and basic set of Value Objects. Production web pages often require more complex queries and Value Objects. Our generation system allows the Value Object, which defines the fields available for display, to be varied independently from the “where” portion of the custom query. A custom query can be used to return an auto-generated default Value Object, and an auto-generated get or getAll can be applied to a custom Value Object. Having them defined separately allows a custom Value Object to be defined once and then reused with many different queries. This is useful for the large number of web pages where the displayed table columns are identical but the specific query is different.
The extensions.xml file is used to define new Value Objects and add custom queries and methods to the SS layer.
Here are some extensions added to the Book table:
<value-object name="BookWithNamesValue" base-table="Book">
<add-column table="Author" column-name="name" />
<add-column table="Author" column-name="penName" />
<add-column table="Publisher" column-name="name" />
</value-object>
This defines a new Value Object. It will contain all the columns of the base table plus the new columns added from the other tables. Generated SQL strings will automatically perform the joins necessary to pull in the other columns. get* and getAll* methods are generated for this new Value Object.
<sql-query-method name="getAllByTitle" value-object="BookWithNamesValue" >
<parameter name="title" />
<where>Book1.title = ?</where>
</sql-query-method>
This defines a new SQL query. It returns the previously defined new Value Object, and restricts its results using the specified SQL where-clause fragment.
<custom-method name="updateStatusByPublisher" table="Book" return-type="void">
This defines a custom method. The actual implementation is placed in a hand-written PHP file, and is invoked by the SS layer.
Pages
The generator automatically produces test add, update, delete, and list web pages for all defined Value Objects, queries, and methods. Production web pages are specified in pages.xml. Supported page types are also add, update, delete, and list.
Here is an example list page for the Book table:
<page name="BookList" type="list" label="Book List"
value-object="BookWithNamesValue" order="Book1.title">
<buttons>
<button label="Add Book" target="BookAdd"/>
<button label="List Books in Stores" target="StoreBookList"/>
</buttons>
<fields>
<field name="update" label="Update" link="BookUpdate" virtual="true"/>
<field name="delete" label="Delete" link="BookDelete" virtual="true"/>
<field name="title" label="Title" link="BookView"/>
<field name="ISBN" />
<field name="author_Name" label="Author Name"/>
<field name="publisher_Name" label="Publisher Name"/>
</fields>
</page>
Here is how the generated web page looks in the browser:

Note this page is using the BookWithNamesValue Value Object defined in extensions.xml. It automatically uses the SS method getAllBookWithNamesValue() (an extension is to use an alternate query method with the parameters read from the request).
The results are ordered by Book.title. The fields refer to the columns by name, or by table_Name in the case of columns added to the Value Object. Labels for all fields default to the column name but can be customized. Here customizations are specified directly; alternately they could be keys into a localization file indexed by user locale.
Hyperlinked buttons are supported both in the body of the table as well as separately. Different styles of hyperlinks (using icons, etc.) are one example of a feature easily added to the generator grammar based on page developer requests.
Here is the xml for an add page for the Book table:
<page name="BookAdd" type="add" label ="Add Book"
value-object="BookValue" success="BookList">
<fields>
<field name="title" label="Title"/>
<field name="ISBN" />
<field name="authorID" label="Author">
<select table="Author" text="name" />
</field>
<field name="publisherID" label="Publisher">
<select table="Publisher" text="name" />
</field>
<field name="status" label="Status"/>
<field name="numCopies" label="Number of Copies"/>
</fields>
</page>
Here is the generated page in the browser:

Note HTML select tags (drop-down-lists) can be specified in the XML simply by listing the desired table and field name. The relevant SQL queries are automatically invoked at run-time. Because the generator has basic schema information available, field validation can be performed automatically:

Currently the system performs required (not null) and integer validation automatically. Single-column unique validation can also easily be added. Maximum text field widths are currently set from the schema.
Note the similarity between the pages.xml specification for a page and a functional specification. Both specify which fields are on a page in what order, which tables fields are taken from, which buttons are on the page, which pages they link to, and what labels are on the fields and buttons. This has several advantages:
- This system has removed all the grunt work from the translation of functional specification into code. The developers can spend their time concentrating on business analysis and back-end business logic implementation instead of HTML, PHP, javascript, field validation, etc.
- The amount of clerical work in connecting the front-end to the back-end is reduced, so there is significantly less chance of an issue with improper field mappings.
- The simple declarative syntax of pages.xml aids communication with non-developer stakeholders on the project.
- Re-targeting the system to other platforms and technologies – WAP, JSP, Swing, etc. is simplified.
The pages.xml specifications are so short because all the layout, etc. decisions have been moved elsewhere in the system. CSS (Cascading Style Sheets) allow the colors, fonts, etc. of HTML elements to be specified in a separate file. But basics of page layout currently need to remain in the HTML file.
By moving the high-level specification of a page to pages.xml the generator controls the placement of page elements. A simple change to the generator template file can alter the button layout, for instance:

Here we have altered the generator with a new layout style. The generator enforces the latest layout style standards so the page developers can concentrate on business functions.
While templating systems alone can accomplish some of these goals, templating combined with generation can create a much more powerful system. Generators can both utilize templating systems in their implementation of generated pages and utilize a templating pattern in XML specification of pages.
Pages in pages.xml can be parameterized and treated as sub-components of other pages. In this way a single page description can be varied and utilized in many different pages. Of course hierarchically composed pages can be constructed from a mixture of generated and hand-written subpages.
Development Style
Currently the generator presented here (and available for download) is a “toy” system. A significant amount of functionality would need to be added to complete a production web site. From my experience performing this extension is a feasible and productive method of developing a working system.
The amount and types of extensions necessary depend on the kind of web site being developed. The production web pages may be nearly suitable as-is for a simple administrative user interface. Customer-facing list pages will need paging mechanisms, editable fields, icons, localization, etc. Headers and footers providing logos, navigation menus (also generatable from a declarative description!), etc. should also be added. Mechanisms for attaching hand-written fragments of PHP code to generated pages will also be needed. These fragments use HTTP get/post and session state variables in preparing query parameters, calling business logic, and controlling navigation flow.
The grammar used in each of the XML files, and especially pages.xml, forms a unique, very high level, declarative language describing your specific application. As new requirements arise during iterative analysis and implementation the grammar is extended (new XML elements and attributes are added). The XML grammar remains concise because it is not trying to be a general-purpose language. The development team will typically be split between business analyst / page author / business logic developers and generator tool developers. The generator tool developers will continuously add new features based on analyst / page author requests. The generator and its grammar co-evolve with your understanding of your application domain. A release of the generator is done when the entire system is ready to ship.
And a big tip for team productivity, for both the analysts and the generator developer: maintain up-to-date DTDs (Document Type Definitions) or XML Schemas for your XML grammar. When anyone has a question about their XML input ask “does it validate?” While none of the Ruby XML APIs currently validate, it is simple to create a 15-line Python script and add it as a build target to your system. DTDs schema.dtd, extensions.dtd, pages.dtd, and samples.dtd have been included with the code. DTDs and XML Schemas allow you to declaratively specify your XML grammar and automatically validate input files against it using the XML parser. Otherwise you have to check for grammatical errors yourself procedurally inside your generator. DTDs and XML Schemas also provide exact documentation of your current grammar for the other developers.
Conclusions
There are many different ways to implement a code generator that generates complete application tiers. I have presented the advantages of a generator that has complete schema information and uses a custom-defined and continuously extended declarative XML grammar. Some advantages I have presented are:
- At the schema level:
- The SQL table creation code and higher level PHP code come from the same source, so they are guaranteed to always be in sync.
- New stereotypes can be used to extend large numbers of tables through simple declarative changes. The generator automatically adds associated functionality to all relevant layers of the system.
- At the database access level:
- Reusable Value Objects and queries can be independently defined and assembled together.
- Value Object (and associated query) definitions automatically track simple schema changes like column addition.
- At the web page level:
- Field validation can automatically be done based on datatype information from the schema.
- Select (drop down) lists can be simply specified.
- New grammar specifying features like new hyperlink types can easily be added.
- Many aspects of page layout may be modified without touching the input XML files.
- New page types and ways of compositing pages together can continuously be added.
- Page definitions resemble high-level functional specifications, easing the work of developers and aiding communications with other stakeholders.
Finally, all of these and many other discussed advantages are possible because we have built our generator around a declarative grammar that focuses on WHAT, not HOW.
Special thanks to Jack Herrington for editorial suggestions.
Add comment November 2, 2007