Model-Driven Development of Web Applications

Tutorial

Gerd Wagner

Brandenburg University of Technology

with Mircea Diaconescu

and Gabriel Diaconita

28 June 2012


Table of Contents

1. Introduction
2. From Domain Modeling via Design to Implementation
I. Platform-Independent Design
3. How to transform a domain model into a design model
Summary in 7 steps
Step 1: Adopt the perspective of the system to be built
4. Representing Associations with Reference Properties
II. Design and Implementation of SQL-92 Databases
5. Designing an SQL-92 Database Schema
Summary in Seven Steps
How to Encode an SQL-92 Database Model
Summary in 3 Steps:
III. Design and Implementation of JavaScript Apps
6. Developing the Model Layer of a JavaScript Application
How to Transform an Association-Free Information Design Model into a JavaScript Model
How to Encode a JavaScript Model
7. Developing a JavaScript LocalSorage-Based Frontend App
Serialization
Writing to LocalStorage
Deserialization
Reading from LocalStorage
8. Developing a Node.js Backend App
IV. Design and Implementation of PHP-Based Zend Framework Applications
9. Developing the Model Layer of a ZF1 App
How to Transform an Association-Free Model into a Zend_Db Model
How to Encode a Zend_Db Model
Define a Table Class for Each Object Type
Define a Table Class for Each Many-to-Many Association
Implement Foreign Key Dependencies
Define a Row Class for Each Object Type
10. Developing the Model Layer of a ZF2 App
Two Infrastructure Classes: TableGateway and RowGateway
How to Transform an Association-Free Model into a ZF2-Zend-Db Model
How to Encode a ZF2-Zend-Db Model
Encode a Zend-Db Table Class for Each Object Type
Encode a Zend-Db Row Class for Each Object Type

List of Figures

4.1. The Publisher-Book-Person design model
4.2. Turn the functional Publisher association end of Book into a corresponding publisher reference property
4.3. Turn a bi-navigable association into two mutual reference properties
4.4. Implement the navigable non-functional association end Book by a multi-valued reference property books
4.5. Implement the bi-navigable many-to-many association bewteen Book and Person by two multi-valued reference properties authors and books
4.6. The association-free design model
5.1. Replacing platform-independent data type names with SQL names
5.2. Eliminate multi-valued attributes
5.3. Turn enumeration datatypes into enum tables
5.4. Handling an enumeration datatype in MySQL
5.5. Eliminate generalization relationships
5.6. Eliminate many-to-many associations
5.7. Eliminate associations
5.8. The complete SQL-92 database model
5.9. The table class Person
5.10. The gender foreign key dependency
6.1. Create setter operations
6.2. The Book class implementation model
6.3. The complete JavaScript implementation model
9.1. The Zend_Db model
10.1. The ZF2-Zend-Db model

List of Tables

5.1. UML stereotypes for SQL-92 models

Chapter 1. Introduction

...

Chapter 2. From Domain Modeling via Design to Implementation

...

Part I. Platform-Independent Design

Chapter 3. How to transform a domain model into a design model

Summary in 7 steps

Text

Step 1: Adopt the perspective of the system to be built

Text

Chapter 4. Representing Associations with Reference Properties

How to transform a design model into an association-free design model, replacing associations by reference properties

Binary associations can be used for modeling reference properties, which are also called roles. E.g., a fathership association from and to a Person class may be used to model the father role. Or the authorship association between the classes Person and Book may be used to to model the single-valued reference property author and the multi-valued reference property authoredBooks.

Object-oriented programming languages, such as JavaScript, PHP and Java, directly support the concept of reference properties, which are properties whose type is a class and whose values are object references. In these languages, binary assocations can be implemented with the help of (possibly multi-valued) reference properties either in one or in both of the participatig classes, depending on the navigability requirements.

Figure 4.1. The Publisher-Book-Person design model

The Publisher-Book-Person design model

Notice that both associations in the Publisher-Book-Person design model are bi-navigable, so they have to be implemented with two mutual reference properties (one representing the association, and the other one representing its inverse).

  1. Map each navigable functional association end to a corresponding functional reference property of the domain class using the name of the association end (or the name of the associated range class, if the association end does not have a name) as the name of the property and the range class as its type.

    Figure 4.2. Turn the functional Publisher association end of Book into a corresponding publisher reference property


  2. In the case of an association that is both functional and inverse functional, and navigable in both directions, this leads to two reference properties, one in each of the two associated classes. Since both of them represent essentially the same information (one of them is the inverse of the other), you must choose which of them is considered the "master", and which of them is the "slave", where the "slave" property is considered to represent the inverse of the "master". The association will be maintained in the class containing the master reference property (with the help of an add and a remove operation). In the other class, the slave reference property representing the (inverse) association is designated as a derived property that is automatically updated whenever the master reference property is updated.

    Figure 4.3. Turn a bi-navigable association into two mutual reference properties


  3. Map each navigable non-functional association end to a corresponding collection-valued property (expressed with the multiplicity symbol *) of its domain class using the pluralized name of the navigable association end as the name of the property and the associated range class as its type.

    Figure 4.4. Implement the navigable non-functional association end Book by a multi-valued reference property books


  4. A many-to-many association that is navigable in both directions is mapped to two multi-valued reference properties, one in each of the two classes participating in the association. Again, in one of the two classes, the multi-valued reference property representing the (inverse) association is designated as a derived property that is automatically updated whenever the corresponding property in the other class (where the association is maintained) is updated.

    Figure 4.5. Implement the bi-navigable many-to-many association bewteen Book and Person by two multi-valued reference properties authors and books


After replacing all associations of the design model with reference properties, we obtain the following association-free model:

Figure 4.6. The association-free design model

The association-free design model

After the platform-independent design model has been completed, one or more platform-specific design models, for a choice of specific implementation platforms, can be derived from it. Such a platform-specific design model ist still a UML class model, but it contains only modeling elements that can be directly encoded in the chosen platform. Thus, for any platform considered, the tutorial contains two sections: 1) how to make the platform-specific design model, and 2) how to encode this model.

Part II. Design and Implementation of SQL-92 Databases

Transforming a platform-independent design model into a relational (i.e. SQL-92) database implementation model requires eliminating all modeling constructs that are not directly supported in SQL-92. This concerns, in particular, generalizations and associations, which are replaced with corresponding foreign key dependencies.

Chapter 5. Designing an SQL-92 Database Schema

How to derive an SQL-92 database schema from an information design model

Recall that by default, a foreign key refers to the primary key of the referenced table, so it can be defined by specifyig the source attribute (list) in the referencing table as well as the referenced table.

For SQL-92 database models, we define the following UML stereotypes:

Table 5.1. UML stereotypes for SQL-92 models
Stereotype Base class Description
«object table» Class A particular kind of class representing a relational database table that is populated with rows representing objects.
«association table» Class A particular kind of class representing a relational database table that is populated with rows representing links between objects, implying that its attributes are foreign keys.
«enum table» Class A particular kind of class representing a single-column relational table that is populated with rows representing enumeration literals.
«pkey» Attribute A particular kind of attribute representing the primary key of a table.
«fkey» Dependency A particular kind of dependency representing a foreign key.
«index» Attribute Designates an attribute/column to be indexed.

Summary in Seven Steps

  1. Replace the standard identifier stereotype stid by the primary key stereotype pkey.

  2. Replace the platform-independent data type names (such as String, Integer, Decimal, Date and Boolean) by SQL-92 data type names. In particular, since SQL-92 does not support the data type Boolean, this requires to use a workaround for it, such as CHAR(1) restricted to the two values ‘y’ and ‘n’.

    Figure 5.1. Replacing platform-independent data type names with SQL names

    Replacing platform-independent data type names with SQL names

  3. Eliminate multi-valued attributes. In general, a multi-valued attribute m in a class A can be replaced by a table M (with the same name as the attribute), having a single column with the same name and type as the attribute, and by a functional association from M to A, as in the following diagram.

    Figure 5.2. Eliminate multi-valued attributes

    Eliminate multi-valued attributes

    In simple cases, multi-valued attributes may be represented by a string-valued attribute. This requires decomposing the string representing a set into its elements by using appropriate string manipulation operators. It only works in simple cases.

  4. Turn any enumeration data type into a corresponding enum table and define suitable foreign keys referencing it in all tables where the enumeration data type has been used (to be drawn as a dependency arrow with the stereotype «fkey»). Insert a corresponding row in the enumeration table for each enumeration literal from the enumeration data type. [1]

    Figure 5.3. Turn enumeration datatypes into enum tables

    Turn enumeration datatypes into enum tables

    If the target DBMS is MySQL, then we can avoid the effort of introducing a special table and a foreign key for implementing an enumeration datatype. Instead, we can use MySQL's non-standard ENUM column type in the following way:

    Figure 5.4. Handling an enumeration datatype in MySQL

    Handling an enumeration datatype in MySQL

  5. Eliminate generalization relationships in the following way:

    1. Copy the standard identifier attribute(s) of the superclass to the subclass.

    2. Replace the generalization arrow by a foreign key dependency.

    Figure 5.5. Eliminate generalization relationships

    Eliminate generalization relationships

  6. Eliminate associations in the following way:

    1. Turn any many-to-many association into a corresponding (association) class that is many-to-one-associated with both involved classes, keeping the names of the association ends. This leads to a model where you have only object classes and functional (or inverse functional) associations, but no more many-to-many associations.

      Figure 5.6. Eliminate many-to-many associations

      Eliminate many-to-many associations

    2. Replace the remaining functional associations with foreign key dependencies (graphically expressed as UML dependency arrows pointing to the class at the functional association end). This requires adding suitable foreign key attributes in the dependent class.

      Figure 5.7. Eliminate associations

      Eliminate associations

  7. Define an index for any attribute that serves as a search field to speed up the execution of certain queries. This may be done with the help of the attribute stereotype «index».[2]

For performance reasons, to speed up the execution of certain queries and certain change operations, it may be a good idea to split certain tables into two or more parts (vertically or horizontally), which may be done in the implementation model. But we will not consider this issue here.

Finally, the complete SQL-92 database model, whiich contains only table classes, foreign key dependencies and constraints, is depicted in the following class diagram:

Figure 5.8. The complete SQL-92 database model

The complete SQL-92 database model

How to Encode an SQL-92 Database Model

Unfortunately, the degree to which standard SQL-92 is supported differs from DBMS to DBMS. Check, e.g., the MySQL reference manual or the SQLite reference manual.

Summary in 3 Steps:

  1. For each table class of the implementation model, write a corresponding CREATE TABLE statement, including column and table constraints. We follow a naming convention where tbale names are lower-case plural. So, for instance, we convert the table class name Person to the table name persons. For a single-attribute primary key, append the SQL keyword PRIMARY KEY in the column declaration clause. For any mandatory attribute, append the NOT NULL constraint keyowrd. For any unique attribute constraint, append the UNIQUE constraint keyowrd. For any attribute constraint attached to the table class, append a corresponding CHECK clause after the column declarations.

    Figure 5.9. The table class Person

    The table class Person

    CREATE TABLE persons(
    personID     INTEGER PRIMARY KEY,
    name         VARCHAR(250) NOT NULL,
    gender       VARCHAR(6) NOT NULL,
    dateOfBirth  DATE NOT NULL,
    dateOfDeath  DATE,
    CHECK (dateOfDeath >= dateOfBirth)
    )
  2. For any foreign key dependency[3], specify the appropriate referential integrity constraint either in the column declaration clause with the help of the REFERENCES keyword (this does not work with MySQL/InnoDB), or in the form of a FOREIGN KEY clause. Notice that for SQLite and other DBMSs, specifying a column name after the name of the target table is optional; if it is not specifed, the foreign key refers to the primary key of the target table. For MySQL, specifying a column name after the name of the target table is mandatory.

    Figure 5.10. The gender foreign key dependency

    The gender foreign key dependency

    CREATE TABLE persons(
    personID     INTEGER PRIMARY KEY,
    name         VARCHAR(250) NOT NULL,
    gender       VARCHAR(6) NOT NULL REFERENCES GenderEL,
    dateOfBirth  DATE NOT NULL,
    dateOfDeath  DATE,
    CHECK (dateOfDeath >= dateOfBirth)
    )

    Alternatively, a foreign key can be defined with a FOREIGN KEY clause after the column declarations:

    CREATE TABLE persons(
    personID     INTEGER PRIMARY KEY,
    name         VARCHAR(250) NOT NULL,
    gender       VARCHAR(6) NOT NULL,
    dateOfBirth  DATE NOT NULL,
    dateOfDeath  DATE,
    CHECK (dateOfDeath >= dateOfBirth),
    FOREIGN KEY (gender) REFERENCES GenderEL (genderEL)
    )
  3. If there are any constraints involving several tables in the implementation model, they may be implemented in the form of corresponding CREATE ASSERTION or CREATE TRIGGER statements, if the target DBMS supports these SQL constructs.



[1] Enumeration-valued attributes can also be implemented by the available means of a specific platform. For instance, in MS Access, enumeration-valued attributes can be implemented with the help of the Lookup Wizard and a suitable Validation Rule.

[2] A database index is similar to a book index.  It has a content field and a pointer. A book index has a topic and a page number. While a book index is linear, a database index is typically in the shape of a B+ tree.

[3] Notice that MySQL only supports foreign keys, if the InnoDB engine is used.

Part III. Design and Implementation of JavaScript Apps

Chapter 6. Developing the Model Layer of a JavaScript Application

How to Transform an Association-Free Information Design Model into a JavaScript Model

The starting point for making a JavaScript model is the association-free information design model shown in Figure 4.6 in Chapter 4, which we reproduce below for convenience.

  1. Create setter operations for each non-derived single-valued property in order to have a central place for property constraints, which will be implemented as part of the setter code.

    Figure 6.1. Create setter operations


  2. Create add/remove operations for each multi-valued property: Each multi-valued property prop[*] : C is implemented as an array prop[...] whose elements are of type C. Instead of a setter operation, we need to define an addProp and a removeProp operation for adding/removing elements to/from the array representing the value set. In the case of the Book property authors, this leads to the following extended Book class model:

    Figure 6.2.  The Book class implementation model


Finally, the complete JavaScript implementation model derived from the above association-free model is depicted in the following class diagram:

Figure 6.3.  The complete JavaScript implementation model


How to Encode a JavaScript Model

Summary in 5 steps:

  1. Encode each class as a constructor function : Each class C of the implementation model is encoded by means of a corresponding JavaScript constructor function with the same name C such that all non-derived properties are parameters whose type should be indicated in a comment. This requires to map the platform-independent data type names (such as String, Integer, Decimal, Date and Boolean) to the corresponding names of the implicit JavaScript data types[4] (in particular, this requires to map Integer and Decimal to Number.).

    In the constructor body, we first assign default values to the class properties. These values will be used when the constuctor is invoked as a default constructor, that is, without arguments. If the constructor is invoked with arguments, these values may be overwritten by calling the setter functions for all class properties. It is good practice to put the optional parameters of the constructor functions at the end of the parameter list, so as to allow invoking the constructor without providing values for these optional parameters.

    For instance, the Person class from the JavaScript implementation model is encoded in the following way:

    function Person(
        personID,    // Number (Integer)
        name,        // String
        dateOfBirth, // Date
        dateOfDeath  // Date or null
        ) {
      // set the default values for the parameter-free default constructor
      this.personID = 0;
      this.name = "";
      this.dateOfBirth = null;
      this.dateOfDeath = null;
      this.authoredBooks = [];
    
      if (arguments.length > 0) {  // contructor with parameters
        try {
          this.setPersonID( personID);
          this.setName( name);
          this.setDateOfBirth( dateOfBirth);
          this.setDateOfDeath( dateOfDeath);
        } catch(e) {
          console.log( e.name + ": " + e.message);
        }
      }
    }

    Since the setters may throw constraint violation exceptions, they are called in a try-catch block where the catch clause takes care of displaying suitable error messages.

  2. Encode the setters as public functions with the help of JavaScript's prototype mechanism. Take care that all constraints of the property as specified in the model are properly encoded in the setter function. This concerns, in particular, the mandatory value and uniqueness constraints implied by the <<stdid>> declaration, and the mandatory value constraints for all properties with multiplicity 1 (which is the default when no multiplicity is shown). If any constraint is violated, a constraint violation exception is thrown, using one of the exception classes MandatoryValueConstraintViolation, TypeConstraintViolation, ValueRangeConstraintViolation, or OtherConstraintViolation, which have to be defined as subclasses of the predefined exception class Error.

    For the setter operation setPersonID we obtain the following code:

    Person.prototype.setPersonID = function( personID) {
      if (personID === undefined) {
        throw new MandatoryValueConstraintViolation("A value for person ID must be provided!");
      } else if (!util.Number.isInteger( personID)) {
        throw new TypeConstraintViolation("The person ID must be an integer!");
      } else {
        this.personID = personID;
      }
    }

    Notice that a utility function isInteger is used for testing if a value is an integer.

    For the setter operation setDateOfDeath we obtain the following code:

    Person.prototype.setDateOfDeath = function( dateOfDeath) {
      // since dateOfDeath is optional, it may be null or undefined
      if (dateOfDeath === null || dateOfDeath === undefined) return;
      if (!(dateOfDeath instanceof Date)) {
        throw new TypeConstraintViolation("Parameter must be of type Date!");
      } else if (!util.Date.isDateAfter( dateOfDeath, this.dateOfBirth)) {
        throw new ValueRangeConstraintViolation("The date of death must be after the date of birth!");
      } else {
        this.dateOfDeath = dateOfDeath;
      }
    }
  3. Encode the add/remove operations as public functions with the help of JavaScript's prototype mechanism. Take care that all constraints of the multi-valued property as specified in the model are properly encoded in the add function. Also make sure that in the case of a multi-valued reference property pair implementing a bi-navigable association, both the add and the remove operation also adds/removes corresponding references to/from the value set of the slave property.

    For the add operation addAuthor in the Book class, which takes care of the maintenance of the multi-valued reference property authors implementing the bi-navigable authorship association betweee Book and Person, we obtain the following code:

    Book.prototype.addAuthor = function( author) {
      if (!(author instanceof Person)) {
        throw new TypeConstraintViolation("Parameter is not of type Person");
      } else {
        this.authors.push( author);
        // automatically update the inverse association represented by the "Person" property "authoredBooks"
        author.authoredBooks.push( this);
      }
    }

    For the remove operation removeAuthor we obtain the following code:

    Book.prototype.removeAuthor = function( author) {
      if (!(author instanceof Person)) {
        throw new TypeConstraintViolation("Parameter is not of type Person");
      } else {
        if (util.Array.removeElement( author, this.authors)) {
          // automatically update the inverse association represented by the "Person" property "authoredBooks"
          util.Array.removeElement( this, author.authoredBooks);
        }
      }
    }

    An object reference is removed from the value set of the multi-valued reference property with the help of the utility procedure util.Array.removeElement defined in the util namespace object.

  4. Encode any other operation of the form op( in prop1) of the class C as a public JavaScript function with the help of the JavaScript prototype mechanism in the form of C.prototype.op = function( prop1) {...}.

  5. Implement any specialization where a class A is specializing a class B by means of the JavaScript code:

    A.prototype = new B();

    In the above case of TextBook specializing Book this leads to:

    TextBook.prototype = new Book();


[4] JavaScript has the following implicit primitive data types: Number, String, and Boolean. In addition, it has the reference type Object, specialized by Function and Array, and by pre-defined subtypes of Object such as Date.

Chapter 7. Developing a JavaScript LocalSorage-Based Frontend App

For building a localSorage-based frontend app we extend the entity classes of the model layer by adding a serialization method and a static deserialization method.

Serialization

Add a serialization method to any entity classes of the model layer. In this method, we take the following steps for turning an object into an object row string:

  1. Flaten the object into an object row by replacing all object references with corresponding IDs (in the sense of foreign key values).

  2. Apply JSON.stringify to the flatened object.

We use the following code pattern, exemplified for the serialize method of the entity class Person where we replace the object references of the multi-valued reference property authoredBooks with corresponding ISBN numbers in the personRow:

Person.prototype.serialize = function() {
  var isbns = [];
  for (var i in this.authoredBooks) {
    isbns.push( this.authoredBooks[i].isbn);
  }
  var personRow = {
      personID: this.personID,
      name: this.name,
      dateOfBirth: this.dateOfBirth,
      dateOfDeath: this.dateOfDeath,
      authoredBooks: isbns
      }
  return JSON.stringify( personRow); 
}

Writing to LocalStorage

Notice that for any entity class (say, Person) there is a corresponding array (persons) containing its extension (the set of its instances). We write this extension to the local storage (in a suitable JavaScript file in the controller layer) by first setting up an array of person row strings and then assigning its serialization as the value of the local storage key personTable:

var personRowStringArray = [];
for (var p in persons) {
  personRowStringArray.push( p.serialize);
}
try {
  localStorage["personTable"] = JSON.stringify( personRowStringArray);
} catch (e) {
  if (e == QUOTA_EXCEEDED_ERR) {
    alert('Quota exceeded!');  // handle the error
  }
}

Deserialization

Add a static deserialization method to any entity classes of the model layer. In this method, we take the following steps for retrieving an object from an object row string :

  1. Reconstruct the (flatened) object row from the object row string by applying JSON.parse.

  2. Unflaten the object row by replacing all IDs with corresponding object references.

We use the following code pattern, exemplified for the static deserialize method of the entity class Person where we replace the ISBN numbers in personRow.authoredBooks with corresponding object references:

Person.deserialize = function( personRowString, books) {
  var personRow = JSON.parse( personRowString);
  var pers, isbn;
  try {
    pers = new Person( personRow.personID, personRow.name,
        personRow.dateOfBirth, personRow.dateOfDeath);
  } catch (e) {
    console.log( e.name + " while deserializing person: " + e.message);
    }
  for (var i in personRow.authoredBooks) {
    isbn = personRow.authoredBooks[i];
    // check if a book with this ISBN exists
    if (books[isbn]) { 
      pers.addAuthoredBook( books[isbn]); 
    }
  }
  return pers;
}

Reading from LocalStorage

We retrieve the extension of an entity class (say, Person) in the form of a row strings array (personRowStringsArray) by accessing the value of the corresponding local storage key (personTable) and then applying JSON.parse to it:

  var personRowStringsArray = JSON.parse( localStorage["personTable"]);  

Chapter 8. Developing a Node.js Backend App

Table of Contents

Part IV. Design and Implementation of PHP-Based Zend Framework Applications

The Zend Framework (ZF) is an object-oriented PHP-based component library based on the MVC paradigm. It contains a MVC model component, called Zend_Db, providing access to SQL databases in a generic way. Therefore, a ZF application model incliudes a Zend_Db model, which is derived from the association-free information design model.

We assume that the SQL DBMS used for making the application supports declaring foreign key constraints. Essentially all modern database storage engines, including MySQL's InnoDB and also SQLite, satisfy this condition.

Chapter 9. Developing the Model Layer of a ZF1 App

For ZF1-Zend-Db models, we define the following UML stereotypes:

Stereotype Base class Description
«object type» Class A class representing a business object type.
«association» Class A particular kind of class representing a binary many-to-many association.

How to Transform an Association-Free Model into a Zend_Db Model

The starting point for making a Zend_Db model is the association-free information design model shown in Figure 4.6 in Chapter 4, which we reproduce below for convenience.

  1. Add many-to-many associations: For each many-to-many association (from the information design model), add a class with stereotype <<association>> and foreign key dependencies to the association's domain and range classes.

  2. Add foreign key dependencies for functional associations: For each single-valued reference property (representing a functional association), add a foreign key dependency to the referenced class.

  3. Add accessor methods: .

    1. For any (data-valued) attribute, add a setter method. A getter method is not needed, since the (generated) properties of a Zend_Db row class are public.

    2. For any derived reference property, add a getter method.

    3. For any non-derived single-valued reference property, add a setter and a getter method.

    4. For any non-derived multi-valued reference property, add an add and a remove method.

  4. Discard all properties: They are not needed, since Zend_Db will create them automatically by looking up the corresponding database table.

After performing these transformation steps, the following ZF1 Zend_Db model is obtained:

Figure 9.1. The Zend_Db model

The Zend_Db model

How to Encode a Zend_Db Model

In the Zend_Db approach, we have to create two PHP classes, a Zend_Db table class and a Zend_Db row class, for any object type class of the Zend_Db model. The purpose of the Zend_Db table class is to facilitate accessing the underlying database table from the ZF/PHP app. The purpose of the Zend_Db row class is to allow defining the accessor methods for reading and writing the attributes of a persistent business object in compliance with its constraints.

  1. Define a table class for each object type: For any object type class in the Zend_Db model, create a corresponding Zend_Db table class.

  2. Define a table class for each many-to-many association: For any <<association>> class in the Zend_Db model, representing a many-to-many association, create a corresponding Zend_Db table class.

  3. Implement foreign key dependencies: For any foreign key dependency in the Zend_Db model, create a corresponding entry in the class-level property $_referenceMap of the referencing table class.

  4. Define a row class for each object type: For any object type class in the Zend_Db model, create a corresponding Zend_Db row class, and implement the accessor methods taking all constraints defined in the model into consideration.

Define a Table Class for Each Object Type

For any object type class in the Zend_Db model, create a corresponding Zend_Db table class. When the name of the object type class is MyObj, we create a Zend_Db table class with name MyObjTable as a subclass of Zend_Db_Table_Abstract. Notice that this is our own naming convention and that the full class names would have the namespace prefix Application_Model_DbTable_.

A Zend_Db table class MyObjTable is defined as a subclass of Zend_Db_Table_Abstract by setting the following class-level properties:

  • $_name to the name of the corresponding SQL database table;

  • $_primary to the name of the primary key attribute;

  • $_sequence to true if the primary key attribute is an automatically incremented counter, and to false otherwise;

  • $_rowClass to the name of the corresponding Zend_Db row class

For being able to retrieve an object/row with a particular key value in a simple way, we define a getter method (with a name like getPublisherByName) for any unique attribute (defining a key), including the primary key attribute. For the primary key attribute, this getter method employs the inherited find method. For other key attributes, it would employ another inherited method, fetchRow, which allows retrieving one or more rows with the help of an SQL WHERE condition.

class PublisherTable 
      extends Zend_Db_Table_Abstract {
  protected $_name = 'Publisher';
  protected $_primary = 'name';
  protected $_sequence = false;
  protected $_rowClass = 'PublisherRow';

  public function getPublisherByName( $name) {
    $rowSet = $this->find( $name);
    return $rowSet[0];
  }
}

Notice that for readability we use relative class names like PublisherTable. Full class names have the (namespace) prefix Application_Model_DbTable_, as in the full class name Application_Model_DbTable_PublisherTable.

Notice also that our MyObjTable class inherits, among others, a method createRow for creating an empty row, a method find for retrieveing rows via their primary key value, and a method fetchAll for retrieveing all rows of the table.

Define a Table Class for Each Many-to-Many Association

For any <<association>> class in the Zend_Db model, create a corresponding Zend_Db table class (without a corresponding row class). When the name of the <<association>> class is MyAssoc, we create a Zend_Db table class with name MyAssocTable as a subclass of Zend_Db_Table_Abstract.

class AuthorshipTable 
      extends Zend_Db_Table_Abstract {
  protected $_name = 'Authorship';
}

For such an association , we only set the class-level property $_name to the name of the corresponding SQL database table, as shown in the example AuthorshipTable above.

Implement Foreign Key Dependencies

For any foreign key dependency in the Zend_Db model, create a corresponding entry in the class-level property $_referenceMap of the referencing table class. Each entry is a key-value pair where the key specifies the name of the foreign key dependency (we normally set it to the name of the foreign key attribute), and its value is an array containing a columns key set to the name of the foreign key attribute, and a refTableClass key set to the name of the referenced table class.

class BookTable 
      extends Zend_Db_Table_Abstract {
  protected $_name = 'Book';
  protected $_primary = 'isbn';
  protected $_sequence = false;
  protected $_rowClass = 'BookRow';
  protected $_referenceMap = array(
    'publisher' => array(
      'columns' => 'publisher',
      'refTableClass' => 'PublisherTable' ));
}						

Define a Row Class for Each Object Type

When the name of the object type class is MyObj, create a Zend_Db row class with name MyObjRow as a subclass of Zend_Db_Table_Row_Abstract. Notice that this is our own naming convention and that the full class names would have the namespace prefix Application_Model_DbTable_.

Zend_Db is using an approach where the properties of the Zend_Db row class are generated automatically by looking up the corresponding column definitions in the SQL database schema. Therefore, only the accesor methods have to be defined in the Zend_Db row class, taking care of all property constraints.

class PublisherRow 
       extends Zend_Db_Table_Row_Abstract {
  public function setName( $name) {
    if (!isset($name) or $name=="") { 
      throw new Exception('Invalid name'); 
    } else $this->name = $name;
  }
  public function setAddress( $address) {
    if (!isset($address) or $address=="") { 
      throw new Exception('Invalid address'); 
    } 
    else $this->address = $address;
  } 
  public function getPublishedBooks() {
    $books = $this->findDependentRowset("BookTable");
    return $books->toArray();
  }
}

For a get method implementing the inverse navigability of a functional association (in the opposite direction of the corresponding foreign key dependency), such as getPublishedBooks in Publisher, we use the pre-defined row class method findDependentRowset.

For a get method implementing the navigability of a functional association (in the direction of the corresponding foreign key dependency), such as getPublisher in Book, we use the pre-defined row class method findParentRow.[5] For a get method implementing the navigability of a many-to-many association, such as getAuthors in Book, or getAuthoredBooks in Person, we use the pre-defined row class method findManyToManyRowset. Both cases are shown in the following example.

class BookRow extends Zend_Db_Table_Row_Abstract {
  ...
  public function getPublisher() {
    return $this->findParentRow('Application_Model_DbTable_PublisherTable');
  }
  public function setPublisher($publisher) {
    if ($publisher != null && (!isset($publisher) 
                              or !($publisher instanceof PublisherRow))) { 
      throw new Exception('Invalid publisher.'); 
    } else if ($publisher == null) {
      $this->publisher = null;
    } else $this->publisher = $publisher->getName();
  }
  public function getAuthors() {   
    $persons = $this->findManyToManyRowset("Application_Model_DbTable_PersonTable", 
                                        "Application_Model_DbTable_AuthorshipTable");
    return $persons->toArray();
  }
  public function addAuthor($person) {
    if (!isset($person) or !($person instanceof PersonRow)) { 
      throw new Exception('Invalid person.'); 
    }
    foreach($this->getAuthors() as $author) {
      if($author->getPersonID() == $person->getPersonID()) {
        throw new Exception('PersonRow is already Author.'); 
      }
    }
    $authorshipTable = new Application_Model_DbTable_AuthorshipTable();
    $authorship = $authorshipTable->createRow();
    $authorship->author = $person->getPersonID();
    $authorship->authoredBook = $this->isbn;
    $authorship->save();
  }
  public function removeAuthor($person) {
    if (!isset($person) or !($person instanceof PersonRow)) { 
      throw new Exception('Invalid person.'); 
    }
    $authorshipTable = new Application_Model_DbTable_AuthorshipTable();
    $authorships = $authorshipTable->find( $this->isbn, $person->getPersonID());
    if($authorships->count() == 1) {
      $authorships->current()->delete();
    } else {
      throw new Exception('PersonRow is not author of book ' . $this->isbn . '.'); 
    }
  }
}


[5] The findParentRow method has as a second optional argument a foreign key name, as defined in the $_referenceMap array. If you don't provide this second argument, then, by default, the first foreign key in the array that references the parent table is used.

Chapter 10. Developing the Model Layer of a ZF2 App

Two Infrastructure Classes: TableGateway and RowGateway

Since in the current beta 4 release of ZF2, Zend-Db does not provide the functionality that we need, especially the Zend-Db RowGateway class is too incomplete and limited, we have to define our own table gateway and row gateway classes, as shown in the following class diagram. Later, when the Zend-Db component of ZF2 is finished, we may have to change this.

As the full name library\Model\TableGateway suggests, we define our classes TableGateway and RowGateway in the namespace library\Model associated with our library module. The code of these classes can be found in the corresponding folders of the model application.

The purpose of our table gateway class is to allow defining the primary key of the table, and to provide methods for getting all rows of the table, as well as for getting and deleting a specific row determined by the primary key value.

The purpose of our row gateway class is to provide methods for inserting and updating rows, using the attribute values from the data array that have beend checked to satisfy all integrity constraints in the corresponding setters.

How to Transform an Association-Free Model into a ZF2-Zend-Db Model

The starting point for making a Zend-Db model is the association-free information design model shown in Figure 4.6 in Chapter 4, which we reproduce below for convenience.

In an application development framework that is using an SQL database management system for persistent information management, such as ZF2, the values of reference properties are normally not internal (Java or PHP) object references, but rather standard identifier (or 'primary key') values. Therefore, e.g., the range of the attribute publisher in Book will be changed from Publisher to string.

  1. Add accessor methods: .

    1. For any (data-valued) attribute, such as name, add a public setter method, setName( name: String). A public getter method, getName(), may be added as well, for retrieving the attribute value from the protected array-valued $data property that any row class inherits from TableRowGateway. For simplicity, however, we omit the standard getter methods for simple attributes, here.

    2. For any reference property, such as as publisher in Book, or /publishedBooks in Publisher, add a getter method. In the case of a single-valued reference property, the getter method returns a row in the form of an associative array with a key for each attribute. In the case of a multi-valued reference property, the getter method returns a ResultSet object.

    3. Only for non-derived reference properties we add suitable change methods:

      1. For any non-derived single-valued reference property, such as publisher in Book, add a setter method that assigns an identifier value, such as a publisher name, to the reference property.

      2. For any non-derived multi-valued reference property, such as authors in Book, add an add and a remove method.

  2. Discard all properties: They are discarded from the model, since they not available explicitly in the corresponding row class, but only implicitly from its array-valued $data property.

After performing these transformation steps, the following ZF2-Zend-Db model is obtained. Notice that we didn't transfer the integrity constraints defined for the properties from the design model to the ZF2-Zend-Db implementation model. This concerns both mandatory value constraints and other constraints expressed as invariants. We have to keep them in mind, and look them up in the association-free design model when we encode our Zend-Db model.

Figure 10.1. The ZF2-Zend-Db model

The ZF2-Zend-Db model

How to Encode a ZF2-Zend-Db Model

We have to create two PHP classes, a Zend-Db table class and a Zend-Db row class, for any class of the ZF2-Zend-Db model. The purpose of the table class is to facilitate accessing the underlying database table from the ZF2 app, including fetching the content of a table and deleting specific rows. The purpose of the row class is to allow defining the setter methods for the attributes of a persistent business object in compliance with its integrity constraints, and using these setter methods when creating and updating objects/rows.

For our simple ZF2 model application, we have defined just one module, called library. The module name is part of the folder path and also of the namespace path. For any object type (class) from our Zend-Db model, say Publisher, we define

  1. a corresponding Zend-Db table class, with name PublisherTable in the PHP class file PublisherTable.php in the folder library/module/library/src/library/Model/;

  2. a corresponding Zend-Db row class, with name PublisherRow in the PHP class file PublisherRow.php in the folder library/module/library/src/library/Model/, and implement the accessor methods taking all constraints defined in the model into consideration;

  3. a corresponding table object factory in the getServiceConfiguration function in Module.php.

Encode a Zend-Db Table Class for Each Object Type

For any object type (class) from our Zend-Db model, we create a corresponding table class. When the name of our object type is MyObj, we create a table class with name MyObjTable extending our TableGateway by providing the following parameter values in the parent constructor invocation:

  1. the name of the corresponding SQL database table;

  2. the name of the primary key attribute (it is assumed that the table has a simple non-composite primary key).

In the following diagram, we show this for the case of the Publisher class.

namespace library\Model;

use library\Model\TableGateway;
use Zend\Db\Adapter\Adapter;

class PublisherTable extends TableGateway {
 public function __construct( Adapter $adapter) {
    parent::__construct("publishers", "name", $adapter);
  }
}

For retrieving or deleting a specific row with a particular primary key value we can use the methods getRowByPrimaryKey or deleteRowByPrimaryKey inherited from the TableGateway class.

Encode a Zend-Db Row Class for Each Object Type

For any object type (class) from our Zend-Db model, we create a corresponding row class. When the name of our object type is MyObj, we create a row class with name MyObjRow extending our RowGateway.

In ZF2-Zend-Db, the attribute-value pairs of a row are represented as elements of the array-valued $data property. In the row class, we define the setter methods that take care of all property constraints and the getter methods for derived properties.

namespace library\Model;

use Zend\Db\Sql\Select
use library\Model\RowGateway;
use library\Model\Exception\MandatoryValueConstraintViolation,
    library\Model\Exception\ValueRangeConstraintViolation,
    library\Model\Exception\TypeConstraintViolation;

class PublisherRow extends RowGateway {
  public function __construct( $tableGateway, $name = null, 
      $address = null) {
    parent::__construct( $tableGateway); 
    try {
      $this->setName( $name);
      $this->setAddress( $address);
    }
    catch (MandatoryValueConstraintViolation $e) {
      die("Missing value: " . $e->getMessage());
    }
    catch (TypeConstraintViolation $e) {
      die("Wrong type: " . $e->getMessage());
    } 
    catch (ValueRangeConstraintViolation $e) {
      die("Out of range: " . $e->getMessage());
    } 	
    catch (\Exception $e) { die( $e->getMessage());}
  }
  public function setName( $name) {
    if (!isset($name) or $name=='') { 
      throw new MandatoryValueConstraintViolation('name'); 
    } else $this->data['name'] = $name;
  }
  public function setAddress( $address) {
    if (!isset($address) or $address=='') { 
      throw new MandatoryValueConstraintViolation('address'); 
    } else $this->data['address'] = $address;
  }
  public function getPublishedBooks() {
    $select = new Select("books");
    $select->where( array("publisher" => $this->name));
    $statement = $this->sql->prepareStatementForSqlObject($select);
    $books = $statement->execute();
    return $books;
  }
}

For a get method implementing the inverse navigability of a functional association (in the opposite direction of the corresponding foreign key dependency), such as getPublishedBooks in Publisher, we prepare an SQL select statement that operates on the associated dependent table books and selects all book rows that reference this publisher.

For a get method implementing the navigability of a functional association (in the direction of the corresponding foreign key dependency), such as getPublisher in Book, we also prepare an SQL select statement that operates on the associated dependent table publishers and selects the publisher row that is referenced by this book.

For a get method implementing the navigability of a many-to-many association, such as getAuthors in Book, or getAuthoredBooks in Person, we again prepare an SQL select statement that now operates on the association table authorships, selecting all authorship rows whose authoredBook value is equal to the ISBN of the current book, and then join this answer set with the persons table using the join condition personID = author.

All these cases are included in the following example.

class BookRow extends RowGateway {
  ...
  public function getPublisher() {
    $select = new Select("publishers");
    $select->where( array("name" => $this->data["publisher"]));
    $statement = $this->sql->prepareStatementForSqlObject( $select);
    $publisher = $statement->execute();
    return $publisher->current();
  }
  public function setPublisher( $publisher) {
    if(!isset($publisher) || $publisher == "") {
      throw new MandatoryValueConstraintViolation('publisher'); 
    } else $this->data["publisher"] = $publisher;
	}
  public function getAuthors() { 
    $select = new Select("authorships");
    $select->columns(array("author"))
           ->where(array("authoredBook" => $this->data["isbn"]))
           ->join("persons", "personID = author");
    $statement = $this->sql->prepareStatementForSqlObject($select);
    $authors = $statement->execute();
    return $authors;
  }
  public function addAuthor( $person) {
    if (!is_int( $person)) { 
      throw new TypeConstraintViolation('Person ID must be an integer!'); 
    }
    // first check if author is already assigned; if yes, do nothing
    $authors = $this->getAuthors();
    foreach( $authors as $author) {
      if($author["personID"] == $person) { return;}
    }
    $insert = new Insert("authorships");
    $insert->values( array("author" => $person, 
                           "authoredBook" => $this->data["isbn"]));
    $statement = $this->sql->prepareStatementForSqlObject($insert);
    $result = $statement->execute();
  }
  public function removeAuthor( $person) {
    if (!is_int( $person)) { 
      throw new TypeConstraintViolation('Person ID must be an integer!'); 
    }
    $delete = new Delete("authorships");
    $delete->where(array("author" => $person, 
                         "authoredBook" => $this->data["isbn"]));
    $statement = $this->sql->prepareStatementForSqlObject($delete);
    $result = $statement->execute();
  }
}