본문 바로가기
PHP
2012.02.14 20:25

The MVC Design Pattern for PHP

조회 수 5915 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

+ - Up Down Comment Print
?

단축키

Prev이전 문서

Next다음 문서

+ - Up Down Comment Print

http://www.tonymarston.net/php-mysql/model-view-controller.html


The Model-View-Controller (MVC) Design Pattern for PHP

By Tony Marston

2nd May 2004
Amended 1st August 2011

As of 10th April 2006 the software discussed in this article can be downloaded from www.radicore.org

Introduction
The Principles of the MVC Design Pattern
- Model
- View
- Controller
- How they fit together
My Implementation
- Component script
- Controller script
- Business Entity class
- XSL Stylesheet
-- XSL Stylesheet for a DETAIL view (vertical)
-- XSL Stylesheet for a LIST view (horizontal)
- XSL (screen) Structure file
-- XSL (screen) Structure file for a DETAIL view (vertical)
--- Field Options
-- XSL (screen) Structure file for a LIST view (horizontal)
Levels of Reusability
Criticisms of my implementation
References
Amendment History

Introduction

I am no stranger to software development having been a software engineer for over 25 years. I have developed in a variety of 2nd, 3rd and 4th generation languages on a mixture of mainframes, mini- and micro-computers. I have worked with flat files, indexed files, hierarchical databases, network databases and relational databases. The user interfaces have included punched card, paper tape, teletype, block mode, CHUI, GUI and web. I have written code which has been procedural, model-driven, event-driven, component-based and object oriented. I have built software using the 1-tier, 2-tier and the 3-tier architectures. I have created development infrastructures in 3 different languages. My latest achievement is to create an environment for building web applications using PHP that encompasses a mixture of 3 tier architecture, OOP, and where all HTML output is generated using XML and XSL transformations. This is documented in A Development Infrastructure for PHP.

Before teaching myself PHP the only occasion where I came in contact with the concept of the model-view-controller design pattern was when I joined a team that was replacing a legacy system with a more up-to-date version that had web capabilities. I was recruited because of my experience with the language being used, but I quickly realised that their design was too clumsy and the time taken to develop individual components was far to long (would you believe two man-weeks for aSEARCH screen and a LIST screen?). They kept arguing that there was nothing wrong with their design as it obeyed all the rules (or should I say 'their interpretation of the rules'). I was not the only one who thought their implementation was grossly inefficient - the client did not like their projected timescales and costs so he cancelled the whole project. How's that for a vote of confidence!

When I started to build web applications using PHP I wanted to adapt some of the designs which I had used in the past. Having successfully implemented the 3-tier architecture in my previous language I wanted to attempt the same thing using PHP. My development infrastructure ended up with the following sets of components:

Although I have never been trained in OO techniques I read up on the theory and used the OO capabilities of PHP 4 to produce a simple class hierarchy that had as much reusable code as possible in a superclass and where each business entity was catered for with a subclass. I published an article Using PHP Objects to access your Database Tables (Part 1) and (Part 2) which described what I had done, and I was immediately attacked by self-styled OO purists who described my approach as "totally wrong" and "unacceptable to REAL object-oriented programmers". I put the question to the PHP newsgroup and asked the opinion of the greater PHP community. This generated a huge response that seemed to be split between the "it is bad" and "it is good" camps, so I summarised all the criticisms, and my responses to those criticisms, in a follow-up article entitled What is/is not considered to be good OO programming.

I have absolutely no doubt that there will be some self-styled MVC purists out there in Internet land who will heavily criticise the contents of this document, but let me give you my response in advance:

I DO NOT CARE! raspberry1 (2K)

I have read the principles of MVC and built software which follows those principles, just as I read the principles of OOP and built software which followed those principles. The fact that my implementation is different from your implementation is totally irrelevant - it works therefore it cannot be wrong. I have seen implementations of several design patterns which were technical disasters, and I have seen other implementations (mostly my own) of the same design patterns which were technical successes. Following a set of principles will not guarantee success, it is how you implement those principles that separates the men from the boys. The principles of the various design patterns are high-level ideas which are language-independent, therefore they need to be translated into workable code for each individual language. This is where I have succeeded and others have failed. I have spent the last 25+ years designing and writing code which works, and I don't waste my time with ideas that don't work. Other people seem to think that following a set of rules with blind obedience is the prime consideration and have no idea about writing usable or even efficient code. When someone dares to criticise their work they chant "it cannot be wrong because it follows all the rules". The difference between successful and unsuccessful software is not in the underlying rules or principles, it is how those rules or principles are implemented that counts. Where implementation 'A' makes it is easier and quicker to develop components than implementation 'B', it does not mean that 'A' is right and 'B' is wrong, it just means that 'A' is better than 'B'. Where an implementation works it cannot be wrong - it can only be wrong when it does not work.


The Principles of the MVC Design Pattern

After researching various articles on the internet I came up with the following descriptions of the principles of the Model-View-Controller design pattern:

The MVC paradigm is a way of breaking an application, or even just a piece of an application's interface, into three parts: the model, the view, and the controller. MVC was originally developed to map the traditional input, processing, output roles into the GUI realm:

Input --> Processing --> Output 
Controller --> Model --> View

Model

  • A model is an object representing data or even activity, e.g. a database table or even some plant-floor production-machine process.
  • The model manages the behavior and data of the application domain, responds to requests for information about its state and responds to instructions to change state.
  • The model represents enterprise data and the business rules that govern access to and updates of this data. Often the model serves as a software approximation to a real-world process, so simple real-world modeling techniques apply when defining the model.
  • The model is the piece that represents the state and low-level behavior of the component. It manages the state and conducts all transformations on that state. The model has no specific knowledge of either its controllers or its views. The view is the piece that manages the visual display of the state represented by the model. A model can have more than one view.

Note that the model may not necessarily have a persistent data store (database), but if it does it may access it through a separate Data Access Object (DAO).

View

  • A view is some form of visualisation of the state of the model.
  • The view manages the graphical and/or textual output to the portion of the bitmapped display that is allocated to its application. Instead of a bitmapped display the view may generate HTML or PDF output.
  • The view renders the contents of a model. It accesses enterprise data through the model and specifies how that data should be presented.
  • The view is responsible for mapping graphics onto a device. A view typically has a one to one correspondence with a display surface and knows how to render to it. A view attaches to a model and renders its contents to the display surface.

Controller

  • A controller offers facilities to change the state of the model. The controller interprets the mouse and keyboard inputs from the user, commanding the model and/or the view to change as appropriate.
  • A controller is the means by which the user interacts with the application. A controller accepts input from the user and instructs the model and view to perform actions based on that input. In effect, the controller is responsible for mapping end-user action to application response.
  • The controller translates interactions with the view into actions to be performed by the model. In a stand-alone GUI client, user interactions could be button clicks or menu selections, whereas in a Web application they appear as HTTP GET and POST requests. The actions performed by the model include activating business processes or changing the state of the model. Based on the user interactions and the outcome of the model actions, the controller responds by selecting an appropriate view.
  • The controller is the piece that manages user interaction with the model. It provides the mechanism by which changes are made to the state of the model.

In the Java language the MVC Design Pattern is described as having the following components:

  • An application model with its data representation and business logic.
  • Views that provide data presentation and user input.
  • A controller to dispatch requests and control flow.

The purpose of the MVC pattern is to separate the model from the view so that changes to the view can be implemented, or even additional views created, without having to refactor the model.

How they fit together

The model, view and controller are intimately related and in constant contact. Therefore, they must reference each other. The picture below illustrates the basic Model-View-Controller relationship:

Figure 1 - The basic MVC relationship

model-view-controller-01 (3K)

Even though the above diagram is incredibly simple, there are some people who try to read more into it than is really there, and they attempt to enforce their interpretations as "rules" which define what is "proper" MVC. To put it as simply as possible the MVC pattern requires the following:

  • It must have a minimum of three components, each of which performs the responsibilities of either the Model, the View or the Controller, as identified above. You may include as many additional components as you like, such as a Data Access Object (DAO) to communicate with a relational database.
  • There is a flow of data between each of those components so that each may carry out its designated responsibilities on that data. What is not specified in MVC is how the mechanics of that flow are to be implemented. The data may be pushed by the Model into the View, it may be pulled by the View from the Model, or pulled into an intermediary (such as the Controller or an Observer) before it is pushed into the View. It does not matter how the data flows, just that it does flow. The actual mechanics are an implementation detail and can therefore be left to the implementor.
  • The MVC pattern does not identify from where each of these components is instantiated and called. Is there a mystical 4th component which oversees the 3 others, or can one of them oversee and direct the other two? As this is not directly specified in MVC the actual mechanics can be treated as a separate implementation detail.

Note the following:

  • HTML output can only be generated and output in one place - the View.
  • Business rules can only be applied in one place - the Model.
  • SQL statements can only be generated and executed in one place - the DAO (which may be inside the Model).

There are some people who criticise my interpretation of the "rules" of MVC as it is different from theirs, and therefore wrong, but who is to say that their interpretation is the only one that should be allowed to exist?


My Implementation

In my implementation the whole application is broken down into a series of top-level components which are sometimes referred to as transactions, tasks or actions. Each transaction component references a single controller, a single view (optional - a transaction may not have a view of its own, in which case it returns control to a transaction which does have a view), and one or more models. The component is self-executing in that it deals with both the HTTP GET and POST requests.

Figure 2 - My implementation of the MVC pattern (1)

model-view-controller-02 (7K)
  • The model is implemented as a series of business entity classes each of which contains all the properties and methods required by a single business entity. It is actually a subclass to an abstract superclass which contains properties and methods which are common to all database tables. The actual generation of DML (Data Manipulation Language) statements is performed in a specialised sister DML class which is sometimes referred to as a Data Access Object (DAO). This allows the application to be switched from one RDBMS to another simply by switching to another DML class. Only the DML class deals with database resources - all communication with the entity subclass is by standard PHP arrays.
  • The view is implemented as a series of screen structure scripts which are combined with the output from each database table class to produce an XML file which is then transformed into HTML output by using one of the generic XSL stylesheets. For each database table there is typically a list view (containing multiple occurrences with data arranged horizontally) and a detail view (containing a single occurrence with data arranged vertically). There are separate transaction patterns to create PDF output in either LIST view or DETAIL view, each with its own report structure file.
  • The controller is implemented as a series of component scripts which link to one of a series of transaction controller scripts. Unlike some implementations which require a separate controller for each transaction, each of my controllers is for a class (or type) of transaction, so the same controller script can be shared by all transactions of the same class. Each of these scripts deals with the following:
    • It handles the HTTP GET and POST requests.
    • It instantiates an object for each business entity identified by the component script.
    • It calls methods on those objects as appropriate. Most will perform some sort of communication with the database although some may not. Data is passed into and out of these objects as standard PHP arrays. In this way an object can deal with any number of database occurrences both as input or output.
    • It builds an XML file using all the data assembled by the objects within its domain. As well as data from the business entities it may also include data for themenu barnavigation baraction barpagination and scrolling areas.
    • It performs an XSL transformation to convert the contents of the XML file into HTML.
    • Some controllers will output directly to a PDF file instead of going down the XML/XSL/HTML route.

Figure 3 - My implementation of the MVC pattern (2)

model-view-controller-03 (6K)

Component script

Every application component will have one of these scripts. It identifies the model (business entity), the view (screen structure file) and the controller (transaction pattern), as shown in the following example:

<?php
//*****************************************************************************
// This will allow an occurrences of a database table to be displayed in detail.
// The identity of the selected occurrence is passed down from the previous screen.
//*****************************************************************************

$table_id = "mnu_user";                      // identifies the model
$screen   = 'mnu_user.detail.screen.inc';    // identifies the view

require 'std.enquire1.inc';                  // activates the controller

?>

Where the controller requires access to more than one business entity, such as in a parent-child or one-to-many relationship, I use such names as $parent_id and$child_id.

The same screen structure file can used by components which access the same business entity but in different modes (insert, update, enquire, delete and search) as the XSL stylesheet is provided with a $mode parameter which enables it to determine whether fields should be read-only or amendable. It is also possible for individual fields within a component to be made read-only by setting the noedit attribute in the XML document, or by removing a field altogether by setting the nodisplay attribute in the XML document. Note that no component is allowed to have more than one view. Some components may have no view at all - they are called upon to take some particular action after which they return control to the calling component which refreshes its own view accordingly.

Controller script

There is a separate controller script for each of the patterns identified in Transaction Patterns for Web Applications. By simply changing the name of the controller script the whole character of the component will change.

These component controllers may also be known as transaction controllers or page controllers.

<?php
// name = std.enquire1.inc

// type = enquire1

// This will display a single selected database occurrence using $where
// (as supplied from the previous screen)

require 'include.inc';

// identify mode for xsl file
$mode = 'enquire';

// define action buttons
$act_buttons['quit'] = 'QUIT';

// initialise session
initSession();

// look for a button being pressed
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
   if (isset($_POST['quit']) or (isset($_POST['quit_x']))) {
      // quit this screen, return to previous screen
      scriptPrevious();
   } // if
} // if

// retrieve profile must have been set by previous screen
if (empty($where)) {
   scriptPrevious('Nothing has been selected yet.');
} // if
   
// create a class instance for the main database table
require "$table_id.class.inc";
$dbobject = new $table_id;

// check that primary key is complete
$dbobject->checkPrimaryKey = TRUE;

// get data from the database
$fieldarray = $dbobject->getData($where);
   
if ($dbobject->getErrors()) {
   // some sort of error - return to previous script
   scriptPrevious($dbobject->getErrors());
} // if
	
// check number of rows returned
if ($dbobject->getNumRows() < 1) {
   scriptPrevious('Nothing retrieved from the database.');
} // if
if ($dbobject->getNumRows() > 1) {
   $message[] = 'Multiple rows selected - only the 1st accepted';
} // if
	
// get any extra data and merge with $fieldarray
$fieldarray = $dbobject->getExtraData($fieldarray);
	
// build list of objects for output to XML data
$xml_objects[]['root'] = &$dbobject;

// build XML document and perform XSL transformation
buildXML($xml_objects, $errors, $message);
exit;

?>

The controller does not know the names of the business entities with which it is dealing - these are passed down as variables from the component script. Each controller expects to deal with a particular variable name or set of variable names. It will append the standard '.class.inc' to the contents of these variables and instantiate objects from them.

The controller does not know the names of any fields with which it is dealing - what comes out of the business entity is a simple associative array of 'name=value' pairs which is passed untouched to a function that will transfer the entire array to the XML document.

In any INPUT or UPDATE components the entire contents of the POST array is passed to the business entity as-is instead of one field at a time. Again this means that no field names have to be hard-coded into any controller script, thus increasing their re-usability.

Business Entity class

Each business entity is implemented as a separate class so that I can encapsulate all the properties and methods of that entity in a single object. As each of these business entities also exists as a database table each class requires code to communicate with that database table. As the code to communicate with the database is virtually the same regardless of which table is being accessed I have isolated all the sharable common code and placed it within a generic table class. I have further isolated all to calls to a particular DBMS engine into a separate DML class so I can switch from one DBMS to another simply by switching to another DML class.

The class methods used by the controller script are also generic. Depending on the controller any of the following class methods may be called:

  • getData($where) - this will retrieve all those occurrences that satisfy the selection criteria in $where. This is used in all LIST, UPDATE, ENQUIRE and DELETE screens.
  • getInitialData($where) - get initial data before a new occurrence is added to the database.
  • insertRecord($_POST) - add a new record to the database.
  • updateRecord($_POST) - update an existing record.
  • validateDelete($where) - validate that a record can be deleted.
  • deleteRecord($where) - delete an existing record.

This means that each business entity class need contain only those properties and methods which are unique to that entity. Everything else can be inherited from thegeneric table class. As a bare minimum each entity subclass need contain only the following:

require_once 'default_table.class.inc';
class Sample extends Default_Table
{
    // additional class variables go here
    var $stuff;
    
    function Sample ()
    {
        $this->tablename       = 'sample';
        $this->dbname          = 'foobar';
        
        // create an array of field names, each with its own array of properties
        $this->fieldspec['fieldname1'] = array('keyword1' => 'value',
                                               'keyword2' => 'value');
        $this->fieldspec['fieldname2'] = array('keyword1' => 'value',
                                               'keyword2' => 'value');
		    
        // primary key details 
        $this->primary_key = array('field1','field2');
        
        // create an array of unique/candidate keys (optional)
        // each unique key may contain one or more fields
        $this->unique_keys[] = array('fieldname1', 'fieldname2');

        // list of optional relationships (where this table is ONE in ONE-to-MANY)
        $this->child_relations[] = array('many' => 'tblchild',
                                         'type' => 'restricted/cascade/nullify',
                                         'fields' => array('one_id' => 'many_id'));
		
        $this->child_relations[] = array(...);
    
        // list of optional relationships (where this table is MANY in ONE-to-MANY) 
        $this->parent_relations[] = array('parent' => 'tblparent',
                                          'fields' => array('many_id' => 'one_id'));

        $this->parent_relations[] = array(...);
        
        // determines if database updates are recorded in an audit log 
        $this->audit_logging = TRUE/FALSE;
        
        // default sort sequence 
        $this->default_orderby = 'fieldname1,fieldname2,...';

    } // end class constructor

} // end class

As of June 2005 I have created a new method of generating the class files for each business entity which is described in A Data Dictionary for PHP Applications. This provides the following facilities:

  • IMPORT - Populate the dictionary database using details extracted from the physical database schema.
  • EDIT - Modify the details for use by the application.
  • EXPORT - Make the details available to the application in the form of disk files which can be referenced by means of the include() function. Two files will be created for each database table:
    • <tablename>.class.inc - this is the initial class file for the database table. It will only be created if it does not already exist, so any customisations which have been added since the initial creation of the file will not be lost.
    • <tablename>.dict.inc - this contains the column details, key details and relationship details. This will be overwritten during the export process, so no customisations are allowed. All customisation should be performed in the dictionary and then exported to this file. Alternatively it is possible to make temporary customisations at runtime by placing code in the _cm_changeConfig method of the relevant table subclass.

The variables in the class constructor do not contain application data (that which passes through the object between the user and the database) but instead contain information about the application data. Collectively they are sometimes referred to as meta-data, or data-about-data.

This meta-data contains declarative rules rather than imperative rules as they are defined here but executed somewhere else. When the time comes (i.e. when the user presses a SUBMIT button on an HTML form) the user data is passed to a validation object along with the meta-data, and it is the responsibility of the validation object to ensure that the user data conforms to those rules. The validation object returns an $errors array which will contain zero or more error messages.

XSL Stylesheet

I have a series of reusable XSL stylesheets each of which will present the data in one of the structures described in Transaction Patterns for Web Applications. These stylesheets are reusable because of one simple fact - none of them contain any hard-coded table or field names. Precise details of which elements from the XML fileare to be displayed where are provided within the XML document itself from data obtained from a screen structure script. This is a great improvement over my original software which required a separate version of a stylesheet for each component.

The basic features of these stylesheets are contained within the DETAIL view and the LIST view. All the others contain the same basic elements in different variations and combinations.

XSL Stylesheet for a DETAIL view (vertical)

This is a DETAIL view which displays the contents of a single database occurrence in columns going vertically down the page with labels on the left and data on the right, as shown in Figure 4.

In most cases the standard 2-column view will be sufficient, but there may be cases when it is desired to have more than one piece of data on the same horizontal line, as shown in Figure 5.

It is important to note that it is not a simple case of adding in extra columns to a row. An HTML table expects each row to have the same number of columns, so it is necessary to make adjustments on all other rows otherwise they will contain empty columns. Notice the following points about Figure 5:

  • 'data1' has been told to span 3 columns, otherwise it would be no wider than 'data2a'.
  • Line 2 contains two labels and two fields. This dictates that each row must therefore contain 4 columns.
  • 'data4b' does not have a label. This is allowed, but it is not possible to have a label without data.
  • 'data4b' has been told to span 2 columns, otherwise it would be no wider than 'label2b'.

In an HTML table it is possible to have a single piece of data which spans more than one column, as shown in Figure 5. It is also possible to have a single piece of data which spans more than one row, as shown in Figure 6.

Notice here that the effect of spanning multiple rows requires data in the other columns in each of the rows that are being spanned.

Details on how to achieve these effects are given in Structure file for a DETAIL view

The same DETAIL view can be used by different components which access the same business entity in different modes (insert, update, enquire, delete and search) as the controller script will pass its particular mode to the XSL stylesheet during the transformation process. The XSL stylesheet will use the mode value to alter the way that it deals with individual fields by making them read-only or amendable as appropriate.

Here is a sample of the XSL stylesheet which provides a DETAIL view:

<?xml version='1.0'?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/1999/xhtml">

<xsl:output method="xml"
            indent="yes"
            omit-xml-declaration="yes"
            doctype-public = "-//W3C//DTD XHTML 1.0 Strict//EN"
            doctype-system = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
/>

<!-- include common templates -->
<xsl:include href=""std.buttons.xsl"/>
<xsl:include href=""std.column_hdg.xsl"/>
<xsl:include href=""std.data_field.xsl"/>
<xsl:include href=""std.head.xsl"/>
<xsl:include href=""std.pagination.xsl"/>

<!-- get the name of the MAIN table -->
<xsl:variable name="main" select="//structure/main/@id"/>
<xsl:variable name="numrows">1</xsl:variable>

<xsl:template match="/">

  <html xml:lang="{/root/params/language}" lang="{/root/params/language}">
    <xsl:call-template name="head" />
  <body>
  
  <form method="post" action="{$script}">
  
    <div class="universe">
    
      <!-- create help button -->
      <xsl:call-template name="help" />
      
      <!-- create menu buttons -->
      <xsl:call-template name="menubar" />
      
      <div class="body">
        
        <h1><xsl:value-of select="$title"/></h1>
        
        <!-- create navigation buttons -->
        <xsl:call-template name="navbar_detail" />
        
        <div class="main">
          <!-- table contains a row for each database field -->
          <table>
        
            <!-- process the first row in the MAIN table of the XML file -->
            <xsl:for-each select="//*[name()=$main][1]">
            
              <!-- display all the fields in the current row -->
              <xsl:call-template name="display_vertical">
                <xsl:with-param name="zone" select="'main'"/>
              </xsl:call-template>
              
            </xsl:for-each>
            
          </table>
        </div>
        
        <!-- look for optional messages -->
        <xsl:call-template name="message"/>
        
        <!-- insert the scrolling links for MAIN table -->
        <xsl:call-template name="scrolling" >
          <xsl:with-param name="object" select="$main"/>
        </xsl:call-template>
        
        <!-- create standard action buttons -->
        <xsl:call-template name="actbar"/>
        
      </div>
      
    </div>
    
  </form>
  </body>
  </html>

</xsl:template>

</xsl:stylesheet>

Full a full breakdown of the different parts within the XSL stylesheet please refer to Reusable XSL Stylesheets and Templates.

XSL Stylesheet for a LIST view (horizontal)

This is a LIST view which displays the contents of several database occurrences in horizontal rows going across the page. The top row contains column headings (labels) while the subsequent rows contain data, each row from a different database occurrence.

Here is a sample of the XSL stylesheet which provides a LIST view:

<?xml version='1.0'?>
<xsl:stylesheet version="1.0"
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
                xmlns="http://www.w3.org/1999/xhtml">

<xsl:output method="xml"
            indent="yes"
            omit-xml-declaration="yes"
            doctype-public = "-//W3C//DTD XHTML 1.0 Strict//EN"
            doctype-system = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
/>

<!-- include common templates -->
<xsl:include href=""std.buttons.xsl"/>
<xsl:include href=""std.column_hdg.xsl"/>
<xsl:include href=""std.data_field.xsl"/>
<xsl:include href=""std.head.xsl"/>
<xsl:include href=""std.pagination.xsl"/>

<!-- get the name of the MAIN table -->
<xsl:variable name="main" select="//structure/main/@id"/>
<xsl:variable name="numrows" select="//pagination/page[@id='main']/@numrows"/>

<xsl:template match="/"> <!-- standard match to include all child elements -->

  <html xml:lang="{/root/params/language}" lang="{/root/params/language}">
    <xsl:call-template name="head" />
  <body>
  
  <form method="post" action="{$script}">
  
    <div class="universe">
      
      <!-- create help button -->
      <xsl:call-template name="help" />
      
      <!-- create menu buttons -->
      <xsl:call-template name="menubar" />
      
      <div class="body">
        
        <h1><xsl:value-of select="$title"/></h1>
        
        <!-- create navigation buttons -->
        <xsl:call-template name="navbar">
          <xsl:with-param name="noshow"   select="//params/noshow"/>
          <xsl:with-param name="noselect" select="//params/noselect"/>
        </xsl:call-template>
        
        <div class="main">
        
          <!-- this is the actual data -->
          <table>
          
            <!-- set up column widths -->
            <xsl:call-template name="column_group">
              <xsl:with-param name="table" select="'main'"/>
            </xsl:call-template>
            
            <thead>
              <!-- set up column headings -->
              <xsl:call-template name="column_headings">
                <xsl:with-param name="table" select="'main'"/>
              </xsl:call-template>
            </thead>
            
            <tbody>
              <!-- process each non-empty row in the MAIN table of the XML file -->
              <xsl:for-each select="//*[name()=$main][count(*)>0]">
              
                <!-- display all the fields in the current row -->
                <xsl:call-template name="display_horizontal">
                  <xsl:with-param name="zone" select="'main'"/>
                </xsl:call-template>
                
              </xsl:for-each>
            </tbody>
            
          </table>
          
        </div>
        
        <!-- look for optional messages -->
        <xsl:call-template name="message"/>
        
        <!-- insert the page navigation links -->
        <xsl:call-template name="pagination">
          <xsl:with-param name="object" select="'main'"/>
        </xsl:call-template>
        
        <!-- create standard action buttons -->
        <xsl:call-template name="actbar"/>
        
      </div>
      
    </div>
  
  </form>
  </body>
  </html>

</xsl:template>

</xsl:stylesheet>

XSL (screen) Structure file

The structure file is a simple PHP script which identifies the XSL stylesheet which is to be used and the user data which is to be displayed by that stylesheet. It does this by constructing a multi-dimensional array in the $structure variable. This has the following components:

xsl_fileThis identifies which XSL stylesheet to use. Note that the same stylesheet can be referenced in many different structure files.
tablesThis associates the name of a zone within the XSL stylesheet with the name of some user data within the XML document. A stylesheet may contain several zones, and the XML document may contain data from several database tables, so it is important to identify which table data goes into which zone. Among the different zone names which you may see are maininneroutermiddle and link.
zone columnsThis allows you to specify attribute values for each table column in this zone.

The following keywords for column attributes are supported in the list/horizontal view:

Note that the 'align', 'valign' and 'class' attributes when added to a <COL> element in the HTML output are poorly supported in most browsers, so will be dealt with as follows:

  • The 'align' and 'valign' attributes will be converted to 'class' when written to the XML output.
  • The 'class' attribute will no longer be added to the <COL> element, but instead will be added to the <TD> element for every cell within the relevant column.

NOTE: if your screen structure file contains two or more of the 'align|valign|class' attributes for the same field then they will be combined automatically into a single 'class' value, so code such as:

$structure['main']['columns'][] = array('width' => '100', 'align' => 'right',
                                                          'valign' => 'middle',
                                                          'class' => 'foobar');

will be treated as if it were:

$structure['main']['columns'][] = array('width' => '100', 'class' => 'right middle foobar');
zone fieldsThis identifies which fields from the database table are to be displayed, and in which order. For a horizontal view (as in Figure 7) this identifies the columns going across the page. For a vertical view (see Figure 4) this identifies the rows going down the page. Note that each element within this array is indexed by a column number (horizontal view) or a row number (vertical view).

The following keywords for additional field attributes are supported in the detail/vertical view:

  • colspan - allow the field to span more than 1 column.
  • rowspan - allow the field to span more than 1 row.
  • cols - will override the value for multiline controls.
  • rows - will override the value for multiline controls.
  • class - specifies one or more CSS classes.
  • align - specifies horizontal alignment (left/center/right).
  • valign - specifies vertical alignment (top/middle/bottom).
  • display-empty - will force a line of labels to be displayed even if all values are missing.
  • imagewidth - will override an image's default width.
  • imageheight - will override an image's default height.
  • javascript - includes optional javascript.

XSL (screen) Structure file for a DETAIL view (vertical)

This is an example of a structure file to produce a standard 2-column DETAIL view as shown in Figure 4.

<?php
$structure['xsl_file'] = 'std.detail1.xsl';

$structure['tables']['main'] = 'person';

$structure['main']['columns'][] = array('width' => 150);
$structure['main']['columns'][] = array('width' => '*');

$structure['main']['fields'][] = array('person_id' => 'ID');
$structure['main']['fields'][] = array('first_name' => 'First Name');
$structure['main']['fields'][] = array('last_name' => 'Last Name');
$structure['main']['fields'][] = array('initials' => 'Initials');
$structure['main']['fields'][] = array('nat_ins_no' => 'Nat. Ins. No.');
$structure['main']['fields'][] = array('pers_type_id' => 'Person Type');
$structure['main']['fields'][] = array('star_sign' => 'Star Sign');
$structure['main']['fields'][] = array('email_addr' => 'E-mail');
$structure['main']['fields'][] = array('value1' => 'Value 1');
$structure['main']['fields'][] = array('value2' => 'Value 2');
$structure['main']['fields'][] = array('start_date' => 'Start Date');
$structure['main']['fields'][] = array('end_date' => 'End Date');
$structure['main']['fields'][] = array('selected' => 'Selected');
?>

Where each row contains only a single label followed by a single field the format of each entry is 'field' => 'label'.

Note that under some circumstances neither the field nor the label will be output, in which case the entire row will be dropped rather than showing an empty row. These circumstances are:

  • The fieldname does not exist in the XML document.
  • The field has the nodisplay attribute set in the $fieldspec array.
  • This behaviour can be overridden by adding 'display-empty' => 'y' to the item in the ['zone']['fields'] array in the screen structure file. This will cause every cell in that row to be output, even if it is empty.

When this information is written out to the XML document it will look something like the following:

  <structure>
    <main id="person">
      <columns>
        <column width="25%"/>
        <column width="*"/>
      </columns>
      <row>
        <cell label="ID"/>
        <cell field="person_id" />
      </row>
      <row>
        <cell label="First Name"/>
        <cell field="first_name"/>
      </row>
      <row>
        <cell label="Last Name"/>
        <cell field="last_name"/>
      </row>
      <row>
        <cell label="Initials"/>
        <cell field="initials"/>
      </row>
      
      ....

      <row>
        <cell label="Start Date"/>
        <cell field="start_date"/>
      </row>
      <row>
        <cell label="End Date"/>
        <cell field="end_date"/>
      </row>
    </main>
  </structure>

Here is an example of a screen structure file that will produce a multi-column DETAIL view as shown in Figure 5:

$structure['main']['fields'][1] = array('person_id' => 'ID',
                                        'colspan' => 5);

$structure['main']['fields'][2][] = array('label' => 'First Name');
$structure['main']['fields'][2][] = array('field' => 'first_name',
                                          'size' => 15);
$structure['main']['fields'][2][] = array('label' => 'Last Name');
$structure['main']['fields'][2][] = array('field' => 'last_name',
                                          'size' => 15);
$structure['main']['fields'][2][] = array('label' => 'Initials');
$structure['main']['fields'][2][] = array('field' => 'initials');

$structure['main']['fields'][4] = array('picture' => 'Picture',
                                        'colspan' => 5,
                                        'imagewidth' => 32,
                                        'imageheight' => 32);
....
$structure['main']['fields'][11] = array('value2' => 'Value 2',
                                         'colspan' => 5);

$structure['main']['fields'][12][] = array('label' => 'Start Date');
$structure['main']['fields'][12][] = array('field' => 'start_date');
$structure['main']['fields'][12][] = array('label' => 'End Date');
$structure['main']['fields'][12][] = array('field' => 'end_date',
                                           'colspan' => 3);

Notice the following differences between this and the standard 2-column view:

  • Each row is numbered explicitly so that multiple entries can be specified for rows 2 and 12.
  • Rows which contain a single label and field (rows 1, 4 and 11 in this example) can be expressed as 'field' => 'label'
  • Rows which require more than one field require a separate entry for each label and each field. Please note the following:
    • The label entry must be defined before the field entry.
    • It is possible to have a field without a label.
    • It is not possible to have a label without a field.
Field Options

Each entry may include any of the following options:

  • colspan - will allow that field to span more than one column in the HTML table. Without it the remaining columns in that row would be blank.
  • rowspan - similar to colspan, but allows the field to extend vertically for more than 1 row.
  • cols - will override the value for multiline controls supplied in the $fieldspec array.
  • rows - will override the value for multiline controls supplied in the $fieldspec array.
  • align - specify horizontal alignment within this cell. See the HTML specification for details.
  • valign - specify vertical alignment within this cell. See the HTML specification for details.
  • size - will set the size of the textbox control for that field when data can be input or amended. This overrides the size value specified in the $fieldspec array. This option is available in both the detail/vertical and list/horizontal views.
  • display-empty - if the field is not present in the XML file then by default neither the label nor the field will be displayed. This option will cause the empty row to be displayed instead of being dropped.
  • class - will allow that label or field to be enclosed in a class attribute, where the class name should be defined in the CSS file.
  • imagewidth - will override the value in the $fieldspec array when an image is displayed. This option is available in both the detail/vertical and list/horizontal views.
  • imageheight - will override the value in the $fieldspec array when an image is displayed. This option is available in both the detail/vertical and list/horizontal views.
  • javascript - see Inserting optional JavaScript for details.

Here is an example of a screen structure file that will produce a multi-column DETAIL view as shown in Figure 6:

$structure['main']['fields'][1] = array('prod_feature_id' => 'ID',
                                        'colspan' => 3);
$structure['main']['fields'][2] = array('prod_feature_cat_id' => 'Category',
                                        'colspan' => 3);

$structure['main']['fields'][3][] = array('label' => 'Measurement Required?', 
                                          'rowspan' => 2);
$structure['main']['fields'][3][] = array('field' => 'measurement_reqd',
                                          'rowspan' => 2);
$structure['main']['fields'][3][] = array('label' => 'Measurement');
$structure['main']['fields'][3][] = array('field' => 'measurement');
$structure['main']['fields'][4] = array('uom_id' => 'Unit of Measure',
                                        'display-empty' => 'y');

$structure['main']['fields'][5] = array('prod_feature_desc' => 'Description',
                                        'colspan' => 3);

Please note the following:

  • The first two entries in row 3 use the rowspan option to extend down into row 4.
  • As the first two cells in row 4 are now occupied the entries for row 4 will now start from cell 3.
  • If there is the possibility that row 4 could be empty thus causing it to be dropped from the display, the 'display-empty' => 'y' entry will cause empty cells to be output instead. This avoids row 5 being moved up to take the position of the missing row 4, which is to the right of the first two cells and immediately below the last 2 cells in row 3.

Please note that additional options may be specified for use with javascript, as shown in RADICORE for PHP - Inserting optional JavaScript - Hide and Seek.

XSL (screen) Structure file for a LIST view (horizontal)

This is an example of a structure file to produce a standard LIST view as shown in Figure 7.

<?php
$structure['xsl_file'] = 'std.list1.xsl';

$structure['tables']['main'] = 'person';

$structure['main']['columns'][] = array('width' => 5);
$structure['main']['columns'][] = array('width' => 70);
$structure['main']['columns'][] = array('width' => 100);
$structure['main']['columns'][] = array('width' => 100);
$structure['main']['columns'][] = array('width' => 100, 'align' => 'center', 'nosort' => 'y');
$structure['main']['columns'][] = array('width' => '*', 'align' => 'right');

$structure['main']['fields'][] = array('selectbox' => 'Select');
$structure['main']['fields'][] = array('person_id' => 'ID');
$structure['main']['fields'][] = array('first_name' => 'First Name');
$structure['main']['fields'][] = array('last_name' => 'Last Name');
$structure['main']['fields'][] = array('star_sign' => 'Star Sign', 'nosort' => 'y');
$structure['main']['fields'][] = array('pers_type_desc' => 'Person Type');
?>

The selectbox entry does not relate to any field from the database. It is a reserved word which causes a checkbox to be defined in that column with the label 'Select'.

The nosort entry (which can be specified in either of the columns and fields arrays) is optional and will prevent the column label from being displayed as a hyperlink which, when pressed, will cause the data to be sorted on that column.

In some cases it might be useful to display an empty column (no heading, no data), and this can be achieved by inserting a line in any of the following formats:

$structure['main']['fields'][] = array('blank' => '');
$structure['main']['fields'][] = array('null' => '');
$structure['main']['fields'][] = array('' => '');

It is also possible to use some of the field options which have been described previously:

  • Options which are added to the ['zone']['columns'] array will appear in the <colgroup> element of the HTML document.
  • Options which are added to the ['zone']['fields'] array will appear in the <td> element of the HTML document.

When this information is written out to the XML document it will look something like the following:

  <structure>
    <main id="person">
      <columns>
        <column width="5"/>
        <column width="70"/>
        <column width="100"/>
        <column width="100">/>
        <column width="100" align="center"/>
        <column width="*" align="right"/>
      </columns>
      <row>
        <cell label="Select"/>
        <cell field="selectbox"/>
      </row>
      <row>
        <cell label="ID"/>
        <cell field="person_id"/>
      </row>
      <row>
        <cell label="First Name"/>
        <cell field="first_name"/>
      </row>
      <row>
        <cell label="Last Name"/>
        <cell field="last_name"/>
      </row>
      <row>
        <cell label="Star Sign"/>
        <cell field="star_sign" nosort='y'/>
      </row>
      <row>
        <cell label="Person Type"/>
        <cell field="pers_type_desc"/>
      </row>
    </main>
  </structure>

Although the contents of the screen structure file is fixed it is possible to make temporary amendments before it is used to build the HTML output. Please refer to the following:


Levels of Reusability

With this arrangement I have achieved the following levels of reusability:

  • Each component (transaction) within the application will have its own component script. This is not sharable.
  • Each business entity will have its own entity class which encapsulates all the properties and methods necessary of that entity. This class can be shared by all components which access that entity.
  • Code that is common to every business entity is contained within a superclass which is shared by every entity subclass through inheritance.
  • Code to physically access a particular RDBMS is isolated in a single DML class. This means that a decision to switch to another database engine will require nothing more than switching to another DML class.
  • Although the XSL (screen) structure file for the LIST screen can only be used by the LIST component, the XSL (screen) structure file for the DETAIL screen can be shared by the INSERT, UPDATE, ENQUIRE, DELETE and SEARCH components.
  • Each generic XSL stylesheet can be shared by multiple components as the table names, field names and field labels are not built into the stylesheet, they are contained with the XML document which is built from data in the XSL (screen) Structure file. For example, I currently have an application containing 350 components, and 220 of them are DETAIL screens which use the same XSL stylesheet.
  • Each controller script can be shared by any component that fits that particular pattern. Thus every LIST 1 component will use the LIST 1 controller, every LIST 2component will use the LIST 2 controller, and every ENQUIRE 1 component will use the ENQUIRE 1 controller. So in my application of 350 components I have 50 of type UPDATE 1, 50 of type ENQUIRE 1, and another 50 of type SEARCH 1. They each share the same controller script that is appropriate for that pattern. This level of reusability with my controller scripts has been achieved because of the following:
    • No class names from which objects are instantiated are hard-coded into any controller - they are passed down as variables by the component script.
    • The method names used to communicate with objects are all generic. I have read about other implementations where objects such as CUSTOMER, PRODUCT and INVOICE have a separate methods called getCustomer()getProduct() and getInvoice(). This means that each object has its own individual get...() method, therefore the controller would have to know which class is being accessed so that it could use the right method for that class. By using generic methods such asgetData()insertRecord()updateRecord() and deleteRecord() this problem is eliminated. This I believe satisfies the OO principle of polymorphism.
    • Rather than having separate getters and setters for each individual field I prefer to use arrays which can contain any number of fields. The input to theinsertRecord() and updateRecord() methods is the contents of the POST array as a single argument. The output from the getData() method is a multi-dimensional array, the first level being indexed by row number and the second level being an associative array of 'name=value' pairs. This entire array is written out to the XML document without any modification or filtering, therefore the controller does not require any knowledge of individual fields.

Criticisms of my implementation

Criticism #1

In this forum post someone asked advice on how the MVC pattern should be implemented, so I decided to offer some based on my experience of having successfully implemented the MVC pattern in a large web application. It seems that quite a few people took exception to my approach as it violated their personal interpretations of the rules of MVC.

In this post I stated the following:

Should the Model transmit its changes directly into the View, should the View extract the changes directly from the Model, or should the Controller extract from the Model and inject into the View? Actually, there is no hard and fast rule here, so any of these options would be perfectly valid.

TomB responded with this:

Well in MVC the view gets its own data from the model. Controller extracting from the model and passing to the view is wrong.

In this post he said:

MVC states that views access the model directly (ie not using the controller as a mediator) and that models should not know of controllers and views.

My point of view is reinforced by How to use Model-View-Controller (MVC) which lists the following in the Basic Concepts section:

In the MVC paradigm the user input, the modeling of the external world, and the visual feedback to the user are explicitly separated and handled by three types of object, each specialized for its task.
  • The view manages the graphical and/or textual output to the portion of the bitmapped display that is allocated to its application.
  • The controller interprets the mouse and keyboard inputs from the user, commanding the model and/or the view to change as appropriate.
  • the model manages the behavior and data of the application domain, responds to requests for information about its state (usually from the view), and responds to instructions to change state (usually from the controller)
The formal separation of these three tasks is an important notion that is particularly suited to Smalltalk-80 where the basic behavior can be embodied in abstract objects: View, Controller, Model and Object. The MVC behavior is then inherited, added to, and modified as necessary to provide a flexible and powerful system.

Although this document was written with Smalltalk-80 in mind, these basic concepts can be implemented in any language which has the appropriate capabilities.

Notice that it concentrates on the responsibilities of the three components, and does nothing but "suggest" how the data may flow between them. Note the use of the term "usually" in the above list, and the phrase "commanding the model and/or the view to change as appropriate." Later in the same document you may find references to particular implementations, but as far as I am concerned these are suggested implementations by virtue of the fact that the words used are cancouldand may. These are not imperatives such as mustshould or will, and as such should not be regarded as required in any implementations.

So even though the MVC pattern does not (or should not, IMHO) dictate how the flow of data should be implemented, whether it should be pulled or pushed, or who should do the pulling or the pushing, TomB seems to think that the only implementations which are allowed are those which have received his personal seal of approval.

Criticism #2

In this post TomB stated:

By state I meant a does the model contain a 'current record set'. Selecting which records to show is clearly display logic (along with ordering and limiting).

I replied in this post with the following:

I disagree (again). The Model contains whatever records it has been told to contain, either by issuing an sql SELECT statement, or by having data injected into it from the Controller. The View does not *request* any particular records from the Model, it deals with *all* records that are currently contained in the Model. Ordering and limiting are data variables which are passed from the Controller, through the Model and to the DAO so that they can be built into the sql SELECT statement. The View does *not* decide how to limit or sort the data - the data which it is given has already been sorted and limited.

TomB argued in this post with the following:

Ordering, limiting and generally deciding which records to show is 100% display logic. By moving this to the model you've reduced reusability of the view.

I don't know about you, but if there are 1,000s of records to display, but the user wants to see them in pages of 10, it is much more efficient, both in time and memory, for the Model/DAO to use limit and offset values so that only the relevant records are actually read from the database. All of these records are then passed to the View, and the View displays every record which it has been given. It just does not sound practical to pass 1,000s of records to the View, then get the View to decide which 10 to display. Similarly it is far easier to sort the records by adding order by to the SQL query than it is to have code to sort them in the View.

Criticism #3

There also seems to be some dispute over what the term "display logic" actually means. A software application can be broken down into the following types of logic:

  • Display logic - code which generates the HTML output that is sent to the user. This exists in, and only in, the View.
  • Business logic - code which implements the business rules. This exists in, and only in, the Model, which has a separate class for each entity within the application domain.
  • Data Access logic - code which constructs and executes SQL queries to read or update data in a database. This exists in, and only in, the Data Access Object (DAO) which is accessed from the Model.
  • Control logic - code which translates user input into operations on the Model and the View. This exists in, and only in, the Controller.

It is clear to me that display logic is that code which directly generates the HTML output. The data which is transformed into HTML may come from any source, but the code which obtains or generates that data is not part of the display logic. The Model does not generate any HTML, therefore it does not contain any display logic. Similarly the Model does not generate and execute any SQL queries, therefore it does not contain any data access logic. This can be summed up as follows:

  • Code which transforms data into HTML is display logic.
  • Code which creates or obtains the data which is subsequently transformed into HTML is not display logic.

I'm sorry if my interpretation of these minor details is different to yours, but just because my interpretation is different does not mean that it is wrong. I still have components which carry out the responsibilities of Model, View and Controller, so as far as I am concerned my version of MVC is perfectly valid. The implementation may be different, but it is allowed to be different.


References


© Tony Marston
2nd May 2004

http://www.tonymarston.net 
http://www.radicore.org

Amendment history:

01 Aug 2011Modified XSL (screen) Structure file to change the way that the 'align' and 'valign' attributes are handled in LIST screens as they are poorly supported in most browsers, and not supported at all in HTML5.
26 Jun 2010Modified How they fit together to include some additional comments. 
Added Criticisms of my implementation.
01 May 2010Modified XSL (screen) Structure file for a DETAIL view to allow the cols and rows attributes.
01 Feb 2010Modified XSL (screen) Structure file for a DETAIL view to allow the align and valign attributes.
01 May 2007Modified XSL (screen) Structure file for a DETAIL view to allow for the addition of a class attribute on both the label and field specifications.
09 Aug 2006Modified XSL (screen) Structure file for a DETAIL view to allow for additional options for use with javascript, as documented in RADICORE for PHP - Inserting optional JavaScript - Hide and Seek.
21 July 2006Modified XSL (screen) Structure to allow for a wider range of attributes on the column specifications.
21 June 2005Modified the contents of XSL Stylesheet and XSL (screen) Structure file.
17 June 2005Moved all descriptions of the contents of the Business Entity Class to A Data Dictionary for PHP Applications.


Title
List of Articles
번호 분류 제목 글쓴이 날짜 조회 수
36 PHP What does PHP keyword 'var' do? Hojung 2013.12.04 3878
35 MySQL How to reset MySQL’s root password on Mac OS X Hojung 2013.04.24 5197
34 PHP easy way would it be to use str_replace() on the whole file (파일내용변경) Hojung 2012.12.12 5511
33 MySQL Mysql SELECT inside UPDATE Hojung 2012.03.28 5341
32 PHP PHP-MySQLi-Wrapper Hojung 2012.03.28 6715
31 PHP MySQL 연결에서 결과값 얻는 PHP 예 (추천) Hojung 2012.03.28 4791
30 PHP MySQL 주요 PHP 함수 (추천) Hojung 2012.03.28 4416
29 PHP Send email using SMTP server's settings Hojung 2012.03.13 4479
28 PHP Send email with mail() Hojung 2012.03.13 4105
27 PHP PHP Upload Progress Bar (APC) Hojung 2012.03.13 4882
26 PHP Rename if exists Hojung 2012.03.12 4229
25 PHP Program execution Functions (exec, system, shell_exec) Hojung 2012.03.11 6008
24 PHP email address validation Hojung 2012.03.11 3954
23 PHP file put contents Hojung 2012.03.01 4492
» PHP The MVC Design Pattern for PHP Hojung 2012.02.14 5915
21 PHP PHP XML 파서 Hojung 2012.02.02 6920
20 MySQL 모든 호스트의 MySQL 액세스 Hojung 2012.01.27 5002
19 MySQL How Do I Enable Remote Access To MySQL Database Server? Hojung 2012.01.27 5704
18 MySQL 특정게시판의 메일리스트 정보 업데이트 Hojung 2011.12.15 6062
17 MySQL Xpressengine 정회원 메일주소만 Hojung 2011.12.15 6288
Board Pagination ‹ Prev 1 2 Next ›
/ 2

Designed by sketchbooks.co.kr / sketchbook5 board skin

나눔글꼴 설치 안내


이 PC에는 나눔글꼴이 설치되어 있지 않습니다.

이 사이트를 나눔글꼴로 보기 위해서는
나눔글꼴을 설치해야 합니다.

설치 취소

Sketchbook5, 스케치북5

Sketchbook5, 스케치북5

Sketchbook5, 스케치북5

Sketchbook5, 스케치북5