《Agile Web Application Development with Yii1.1 and PHP5》

Chapter 5: Iteration 2: Project CRUD

Now that we have a basic application in place and configured to communicate with our database, we can begin to work on some real features of our application. We know that the project is one of the most fundamental components in our application. A user cannot do anything useful with the TrackStar application without first either creating or choosing an existing project within which to add tasks and other issues. For this reason, we want to use our second iteration to focus on getting the project entity wired into the application.

Iteration planning

This iteration is fairly straightforward. At the end of this iteration, our application should allow users to create new projects, select from a list of existing projects, update/edit existing projects, and delete existing projects.

In order to achieve this goal, we need to identify all the more granular tasks on which to focus. The following list identifies a more granular list of tasks we aim to accomplish within this iteration:

  • Design the database schema to support projects
  • Build the needed tables and all other database objects indentified in the schema
  • Create the Yii AR model classes needed to allow the application to easily interact with the created database table(s)
  • Create the Yii controller class(es) that will house the functionality to:
    • Create new projects
    • Fetch a list of existing projects for display
    • Update metadata on existing projects
    • Delete existing projects
  • Create the Yii view files and present their logic in a way that will:
    • Display the form to allow for new project creation
    • Display a list of all the existing projects
    • Display the form to allow a user to edit an existing project
    • Add a delete button to the project listing to allow for project deletion

This is certainly enough to get us started. We will soon be able to put these tasks into our TrackStar application, and manage them from there. For now, I guess we will just have to jot them down in a notebook.

Running our test suite

Before we jump right into development, we should run our existing test suite and make sure all of our tests pass. We only have one test thus far. The test we added in Chapter 4, Iteration 1: Creating the Initial TrackStar Application tests for a valid database connection. So, it certainly won't take too long to quickly run our test suite. Open up your command prompt and from the /protected/tests folder, run all of the following unit tests at once:

%phpunit unit/
PHPUnit 3.3.17 by Sebastian Bergmann.
Time:
::0 seconds
OK (1 test, 1 assertion)

With all of our tests passing, our confidence is boosted. Now we can begin to make changes

Creating the project table

Back in Chapter 3, The TrackStar Application we talked about the basic data that represents a project, and in Chapter 4 we decided that we would use a MySQL relational database to build out the persistence layer of this application. Now we need to turn the idea of project content into a real database table.

We know projects need to have a name and a description. We are also going to keep some basic table auditing information on each table by tracking the time a record was created and updated as well as who created and updated the record. This is enough to get us started and meet the goals of this first iteration.

Based on these desired properties, here is how the project table looks:

CREATE TABLE tbl_project
(
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(128),
    description TEXT,
    create_time DATETIME,
    create_user_id INTEGER,
    update_time DATETIME,
    update_user_id INTEGER
);

Covering third-party database administration tools is outside of the scope of this book. We also want to allow you to follow along while potentially using something other than MySQL. For these reasons, we are going to simply provide the low-level Data Definition Language (DDL) statements for the structures that we create. So, go ahead and open up your favorite database editor within your preferred Yii- supported database server and create this table in the trackstar_dev database that you created in Chapter 4.

Depending on the particular database you choose to use, there are many available tools that help with the maintenance of a database schema and assist in database administration We recommend using a tool that will make things easier when it comes to database administration. We are actually using MySQLWorkbench (http://dev.mysql.com/ downloads/workbench/5.1.html) to design, document, and manage our database schema. We are also using phpMyAdmin (http:// www.phpmyadmin.net/home_page/downloads.php) to help with general administration. There are many similar tools available. The small amount of time it takes to become familiar with how to use them can save you a lot of time in the long run.

Naming conventions

You may have have noticed that we defined our database table as well as all of the column names in lowercase. Throughout our development, we will use lowercase for all table names and column names. This is primarily because different DBMS handle case-sensitivity differently. As one example, PostgreSQL treats column names as case-insensitive by default, and we must quote a column in a query condition if the column contains mixed-case letters. Using lowercase would help eliminate this problem.

You may have also noticed that we used a tbl_ prefix in naming our projects table. As of version 1.1.0, Yii provides integrated support for using table prefix. Table prefix is a string that is pre-pended to the names of the tables. It is often used in shared hosting environments where multiple applications share a single database and use different table prefixes to differentiate from each other. For example, one application could use tbl_ as a prefix while another could use yii_. Also, some database administrators use this as a naming convention to prefix database objects with an identifier as to what type of entity they are, or otherwise to use a prefix to help organize objects into similar groups.

In order to take full advantage of the integrated table prefix support in Yii, one must appropriately set the CDbConnection::tablePrefix property to be the desired table prefix. Then, in SQL statements used throughout the application, one can use {{TableName}} to refer to table names, where TableName is the name of the table, but without the prefix. For example, if we were to make this configuration change we could use the following code to query about all projects:

$sql='SELECT * FROM {{project}}';
$projects=Yii::app()->db->createCommand($sql)->queryAll();

But this is getting a little ahead of ourselves. Let's leave our configuration as it is for now, and revisit this topic when we get into database querying a little later in our application development.

Creating the AR model class

Now that we have the tbl_project table created, we need to create the Yii model class to allow us to easily manage the data in that table. We introduced Yii's Object -relational Mapping (ORM) layer and Active Record (AR), back in Chapter 1, Meet Yii. Now we will see a concrete example of that in the context of this application.

Previously, we used the yiic shell command to help with some autogeneration of code. As we saw in Chapter 2, Getting Started when we were using the shell command to create our first controller, there are many other shell commands you can execute to help auto create application code. However, as of version 1.1.2 of Yii, there is a new and more sophisticated interface available called Gii. Gii is a highly customizable and extensible web-based code generation platform that takes the yiic shell command to new heights. We will be using this new platform to create our new model class.

Configuring Gii

return array(
      'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..',
      'name'=>'My Web Application',
      // preloading 'log' component
      'preload'=>array('log'),
      // autoloading model and component classes
      'import'=>array(
        'application.models.*',
        'application.components.*',
      ),
 
      'modules'=>array(
          'gii'=>array(
                'class'=>'system.gii.GiiModule',
                'password'=>'[add_your_password_here]',
          ),
      ),

This configures Gii as an application module. We will cover Yii modules in detail later in the book. The important thing at this point is to make sure this is added to the configuration file and that you provide your password. Now, navigate to the tool at: http://localhost/trackstar/index.php?r=gii.

The following screenshot shows the authentication form you will be presented with:

Using Gii to create our Project AR class

Go ahead and enter the password you provided during the configuration. A successful entry will take you to the following main menu page of Gii:

As you may recall, these choices are similar to the options we received back in Chapter 2 when typing help within the yiic shell command-line tool. As we want to create a new model class for our tbl_project table, the Model Generator option seems like the right choice. Clicking that link takes us to the following page:

The Table Prefix field is primarily used to help Gii determine how to name the AR class we are generating. If you are using a prefix, you can add this here. This way, it won't use that prefix when naming the new class. In our case, we are using the tbl_ prefix, which also just happens to be what this form field defaults to. So, specifying this value will mean that our newly generated AR class will be named Project, rather than tbl_project.

The next two fields are asking for our Table Name and the name of the class file we want it to generate. Type in the name of our table, tbl_project, and watch as the model class name auto-populates. The convention for the Model Class name is the name of the table, minus the prefix, and starting with an uppercase letter. So, it will assume a name of Project for our Model Class name, but of course you can customize this.

The next few fields allow for further customization. The Base Class field is used to specify the class from which our Model Class will extend. This will have to be CActiveRecord or a child class thereof. The Model Path field allows you to specify where in the application folder structure to output the new file. The default is protected/models/ (also known as application.models). The last field allows us to specify a template on which the generated code is based. We can customize the default one to meet any specific needs we have that might be common to all such class files. For now, the default values for these fields meet our needs just fine.

Proceed by clicking on the Preview button. This will result in the following table that is displayed at the bottom of the page:

This link allows you to preview the code that will be generated. Before you hit Generate, click on the models/Project.php link. The following screenshot displays what this preview looks like:

It provides a nice scrollable popup, so that we can preview the whole file that will be generated.

Okay, close this popup and go ahead and click on the Generate button. Assuming all went well, you should see the following screenshot displayed at the bottom of the page:

Ensure that /protected/models (or whatever folder you specified in the Model Path form field) is writable by your web server process prior to attempting to generate your new model class. Otherwise, you will receive a permissions error.

Gii has created for us a new Yii AR model class, named (as we instructed it to) as Project.php, and placed it (as we instructed it to) in the default Yii location for model classes, protected/models/. This class is a wrapper class for our tbl_project database table. All of the columns in the tbl_project table are accessible as properties of the Project AR class.

Let's get familiar with our newly created AR class by writing some tests.

Testing out our newly generated code

A great way to get familiar with new code or functionality is to write tests against it. Starting with some unit tests is a great way to get a general feel of how AR classes work in Yii. As this iteration is focused on Creating, Reading, Updating, and Deleting (CRUD) projects, we'll write some tests for these operations on the Project AR class. The public methods for each of these CRUD functionalities are already present in our new Project.php AR class, so we won't need to code for those. We can just focus on writing the tests.

Creating the unit test file

First we need to create a new unit test file. Let's create that file here: protected/ tests/unit/ProjectTest.php, and have it contain the following code:

<?php
class ProjectTest extends CDbTestCase
{
  public function testCRUD()
  {
  }
}

The class we have added extends CDbTestCase, which is the Yii Framework base class for unit test classes specifically intended to test database related functionality. This database specific base class provides fixture management, which we will cover in more detail.

We'll use the testCRUD() method for testing all CRUD operations against the Project AR class. We'll start with testing a new Project creation.

We are not actually engaging in TDD as this point. The reason for this is that we are not writing any of the code we are testing. We are using this testing approach to help you get familiar with both AR classes in Yii as well as with writing some basic tests. As this is not really TDD, we will not exactly follow the TDD steps closely as outlined in Chapter 3. For example, because the code we are going to be testing is core Yii Framework code, which works very well, we don't have to do things such as first writing a failing test.

Testing create

Add the following code to the testCRUD() method that creates a new Project AR class, sets its project attributes, and then saves it:

public function testCRUD()
{
  //Create a new project
  $newProject=new Project;
  $newProjectName = 'Test Project 1';
  $newProject->setAttributes(
      array(
                       'name' => $newProjectName,
                       'description' => 'Test project number one',
                       'create_time' => '2010-01-01 00:00:00',
                       'create_user_id' => 1,
                       'update_time' => '2010-01-01 00:00:00',
                       'update_user_id' => 1,
                   )
      );
    $this->assertTrue($newProject->save(false));
}

This code first creates a new Project AR instance by invoking new. We then use the setAttributes() method of the AR class to set the AR class attributes in a bulk way based on an input array. We see that the class properties are the keys to this input array, and they will be set to the values specified in this array.

After setting the attributes, we save the new Project by invoking its save() method. We pass the optional false parameter into the save() method to tell it to bypass any data validation of the attributes (we'll cover model data validation in the section, Adding a required field to our form). We then test to make sure the returned value from saving the new record is true, which indicates a successful save.

Now toggle to the command line to execute this new test to ensure success:

% cd Webroot/protected/tests
% phpunit unit/ProjectTest.php
PHPUnit 3.3.17 by Sebastian Bergmann.
.
Time:
::0 seconds
OK (1 test, 1 assertion)

Great, it passed. So we have successfully added a new project. You can verify this by querying your database directly. Using your preferred database maintenance tool, select back everything from the Project table. You will see that you have a new row with the details that match the attributes we set for the Project AR class. In the following example we used MySQL command line as follows:

mysql> select * from tbl_project\G
*************************** 1. row ***************************
                           id: 1
                   name: Test Project 1
         description: Test project number one
       create_time: 2010-01-01 00:00:00
  create_user_id: 1
     update_time: 2010-01-01 00:00:00
update_user_id: 1
1 row in set (0.00 sec)

You may have noticed that we did not specify the id column when setting the attributes of the Project AR class. This is because the column is defined to be an auto-increment Primary Key. The database automatically assigns this value when inserting new rows. Once the insert is successful, this attribute is properly set in the AR class itself. You could easily access the newly auto-assigned id attribute in the following manner:

$newProject->id

We'll use this in our next test.

Testing read

Now that we have verified the create functionality by testing the save() method of our new Project AR class, let's move on to the read. Add the following highlighted code to the same testCRUD() method, just below where we saved the new record:

public function testCRUD()
{
  //Create a new project
 
  //READ back the newly created project
  $retrievedProject=Project::model()->findByPk($newProject->id);
  $this->assertTrue($retrievedProject instanceof Project);
  $this->assertEquals($newProjectName,$retrievedProject->name);
}

Here we use the static method model() that must be defined in every AR class. This method returns an instance of the Project AR class that is further used to call other class-level methods. We are calling the findByPk() method to retrieve a specific instance of Project.

This method (as you might expect) takes in the Primary Key value and returns the specific row that matches the unique identifier. We feed it the newly created auto increment id attribute of the Project instance we created previously. This way, we are attempting to read back the exact row we inserted when we saved $newProject. We then have two assertions. We first verify that the entity we read back is an instance of the Project AR class. We then verify that the project name of the record read back is the same as the name we gave the project when we initially saved it.

Once again, let's toggle to the command line and run the following test:

% phpunit unit/ProjectTest.php
...
OK (1 test, 3 assertions))) )

Very nice! We have verified that the "R" in CRUD is working as we expect.

Let's move a little more quickly now and test Update and Delete at the same time.

Testing update and delete

Now add the following code at the bottom of the same testCRUD() method we have been using, just after the tests we added for create and read previously:

//Create a new project
...
//READ back the newly created project
...
//UPDATE the newly created project
$updatedProjectName = 'Updated Test Project 1';
$newProject->name = $updatedProjectName;
$this->assertTrue($newProject->save(false));
 
//read back the record again to ensure the update worked
$updatedProject=Project::model()->findByPk($newProject->id);
$this->assertTrue($updatedProject instanceof Project);
$this->assertEquals($updatedProjectName,$updatedProject->name);
 
//DELETE the project
$newProjectId = $newProject->id;
$this->assertTrue($newProject->delete());
$deletedProject=Project::model()->findByPk($newProjectId);
$this->assertEquals(NULL,$deletedProject);

Here we have added the tests for updating and deleting a Project. First we gave the $newProject instance a new and updated name and then saved the project again. As we are dealing with an existing AR instance this time, our AR class knows to do an Update, rather than inserting a new record, whenever we invoke ->save(). We then read back the row to ensure the name was updated.

To test the Delete, we saved the project id into a local variable $newProjectId. We then called the ->delete() method on our AR instance which (as you probably guessed) deletes the record from the database and destroys the AR instance. We then used our local variable holding the project id to attempt to read back the row by this Primary Key. As the record should have been deleted, we expect this result to be NULL. The test asserts that we do get a NULL value returned.

Let's make sure these tests pass. Run the test once again to ensure success:

% phpunit unit/ProjectTest.php 
... 
OK (1 test, 8 assertions)

Thus we have verified that all of our Project AR class CRUD operations are working as expected.

Was all that testing really necessary?

When taking a TDD approach to software development, one is constantly faced with making a decision of what to test and, sometimes more importantly, what parts not to test.

These are questions you have to answer for yourself. You want to test enough to provide maximum confidence in the code, but obviously testing every single line of code in an application can be overkill.

One general rule of thumb is not to worry about testing code in external libraries you did not write (unless you have a specific reason to distrust it).

The CRUD operations we just wrote tests for, against the Project AR class, fall into this category. The code behind them is part of the Yii Framework, and not code that we wrote. We did not write these tests because we distrust the framework code, but rather to get a feel for using Active Record in Yii. A great by-product of this exercise is that we now have this as part of our test suite. However, it is unnecessary to go through this testing exercise for every AR model class we create, and thus we won't be doing so for other AR classes we create.

Enabling CRUD operations for users

The previously mentioned tests introduced us to using AR class instances. It showed us how to use them to create new records, retrieve back existing records, update existing records, and delete existing records. We spent a lot of time testing these lower-level operations on the AR class instance for the Project table, but our TrackStar application does not yet expose this functionality to users. What we really need is a way for users to Create, Read, Update, and Delete projects within the application. Now that we know our way around AR a little, we could start coding this functionality in some controller class. Luckily, we don't have to.

Creating CRUD scaffolding for projects

Once again, the Gii code generation tool is going to rescue us from having to write common, tedious and often time-consuming code. CRUD operations are such a common need of database tables created for applications that the developers of Yii decided to provide this for us. If you are familiar with other frameworks, you may know this by the term scaffolding. Let's see how to take advantage of this in Yii.

Navigate back to the main Gii menu located at http://localhost/trackstar/ index.php?r=gii, and choose the Crud Generator link. You will be presented with the following screen:

Here we are presented with two input form fields. The first one is asking for us to specify the Model Class against which we would like all of the CRUD operations generated. In our case, this is our Project.php AR class we created earlier. So enter Project in this field. As we do this, we notice that the Controller ID field is auto-populated with the name project, based on convention. We'll stick with this default for now.

With these two fields filled in, clicking the Preview button will result in the following table being added to the bottom of the page:

We can see that quite a few files are going to be generated, which include a new ProjectContrller.php controller class that will house all of the CRUD action methods and many separate view files. There is a separate view file for each of the operations as well as one that will provide the ability to search project records. You can, of course, choose not to generate some of these by changing the checkboxes in the corresponding Generate column in the table. However, for our purposes, we would like Gii to create all of these for us.

Go ahead and click the Generate button. You should see the following success message at the bottom of the page:

You may need to ensure that both /protected/controllers, as well as, /protected/views under the root application folder are both writable by the web server process. Otherwise, you will receive permission errors, rather than this success result.

We can now click on the try it now link to take our new functionality for a test drive.

Doing so takes you to the project listing page. This is the page that displays all of the projects currently in the system. You might not expect any to be in there yet, as we have not explicitly created any using our new Create functionality. However, our project listing page does have a few projects displayed as shown in the following screenshot. (For reference, the page can be found here: http://localhost/trackstar/index.php?r=project)

So, where did these projects come from? You might even have more or less than these three that are listed in the preceding screenshot depending on the number of times you reran the unit tests mentioned previously. The unit tests we wrote to test the CRUD operations of our new Project AR class were actually creating new records in the database every time we ran them. These are all of the records that were created before we finished writing our test for deletion, as that test eventually deleted the same record created in the test for creation. In this particular case, it is nice to have a few projects in the system so we can see how they are displayed. However, in general it is a bad idea to run the unit and functional tests against the development database. Soon, we'll cover how to change these tests to run against a separate dedicated test database. For now, let's just keep playing with our newly generated code.

Creating a new project

You'll notice on this project listings page (displayed in the previous screenshot) a little navigation column in the right column block. Go ahead and click on on the Create Project link. You'll discover this actually takes us to the Login page, rather than a form to create a new project. The reason for this is that the code Gii has generated applies a rule that stipulates that only properly authenticated users (that is, logged-in users) can create new projects. Any anonymous user that attempts to access the functionality to create a new project will be redirected to the Login page. Go ahead and log in using the credentials username as demo and password as demo.

A successful login should redirect you to the following URL: http://localhost/trackstar/index.php?r=project/create

This page displays a nice input form for adding a new project, as shown in the following figure:

Let's quickly fill out this form to create a new project. Even though none of the fields are marked as required, let's fill in the Name field as Test Project and the Description field as Test project description. Hitting the Create button will post the form data back to the server, and attempt to add a new project record. If there are any errors, a simple error message will display that highlights each field in error. A successful save will redirect to the specific listing for the newly created project. Ours was successful, and we were redirected to the page with the URL http://localhost/trackstar/index. php?r=project/view&id=4, as shown in the following screenshot:

As was mentioned previously, one thing we notice about our new project creation form is that none of the fields are currently marked as being required. We can successfully submit the form without any data at all. However, we know that every project needs to have at least a name. Let's make this a required field.

Adding a required field to our form

When working with AR model classes within forms in Yii, setting validation rules around form fields is a snap. This is done by specifying values in an array set in the rules() method within the Project AR model class.

Opening up the /protected/models/Project.php class reveals that this public method has already been defined, and that there are already a few rules in there:

/**
  * @return array validation rules for model attributes.
  */
  public function rules()
  {
    // NOTE: you should only define rules for those attributes that will receive user inputs.
    return array(
      array('create_user_id, update_user_id', 'numerical', 'integerOnly'=>true),
      array('name', 'length', 'max'=>128),
      array('create_time, update_time', 'safe'),
      // The following rule is used by search().
      // Please remove those attributes that should not be searched.
      array('id, name, description, create_time, create_user_id, 
      update_time, update_user_id', 'safe', 'on'=>'search'),
    );

The rules() method returns an array of rules. Each rule is of the following general format:

Array('Attribute List', 'Validator', 'on'=>'Scenario List', …additional options);

The Attribute List is a string of comma separated class property names to be validated according to the Validator. The Validator specifies what kind of rule should be enforced. The on parameter specifies a list of scenarios in which the rule should be applied.

Scenarios allow you to restrict the application of a validation to special contexts. A typical example for an active record would be insert or update. For example, if 'on'=>'insert' is specified, this would indicate that the validation rule should only be applied when the model's scenario attribute is insert. The same holds true for 'update' or any other scenario you wish to define. You can set a model's scenario attribute either directly, or by passing it to the constructor when creating a new instance."

If this is not set, the rule is applied in all scenarios when save() is called. Finally, the additional options are name/value pairs, which are used to initialize the Validator's properties.

The Validator can be either a method in the model class, or a separate Validator class. If defined as a model class method, it must have the following signature:

/**  
* @param string the name of the attribute to be validated  
* @param array options specified in the validation rule  
*/ 
public function ValidatorName($attribute,$params) { ... }

If we use a separate class to define the Validator, that class must extend from CValidator. There are actually three ways to specify the Validator in the previously mentioned general format:

  1. One is to specify a method name in the model class itself.
  2. A second is to specify a separate class that is of a Validator type (that is, a class that extends CValidator).
  3. The third manner in which you can define the Validator is by specifying a predefined alias to an existing Validator class in the Yii Framework.

Yii provides many predefined Validator classes for you and also provides aliases with which to reference these when defining rules. The complete list of predefined Validator class aliases as of Yii version 1.1 is as follows:

  • boolean: Alias of CBooleanValidator, ensuring the attribute has a value that is either true or false
  • captcha: Alias of CCaptchaValidator, ensuring the attribute is equal to the verification code displayed in a CAPTCHA
  • compare: Alias of CCompareValidator, ensuring the attribute is equal to another attribute or constant
  • email: Alias of CEmailValidator, ensuring the attribute is a valid e-mail address
  • default: Alias of CDefaultVAlidator, assigning a default value to the specified attributes
  • exist: Alias of CExistValidator, ensuring the attribute value can be found in the specified table column
  • file: Alias of CFileValidator, ensuring the attribute contains the name of an uploaded file
  • filter: Alias of CFilterValidator, transforming the attribute with a filter
  • in: Alias of CRangeValidator, ensuring the data is among a pre-specified list of values
  • length: Alias of CStringValidator, ensuring the length of the data is within certain range
  • match: Alias of CRegularExpressionValidator, ensuring the data matches a regular expression
  • numerical: Alias of CNumberValidator, ensuring the data is a valid number
  • required: Alias of CRequiredValidator, ensuring the attribute is not empty
  • type: Alias of CTypeValidator, ensuring the attribute is of a specific data type
  • unique: Alias of CUniqueValidator, ensuring the data is unique in a database table column
  • url: Alias of CUrlValidator, ensuring the data is a valid URL

As we want to make the project name attribute a required field, it looks like the required alias will meet our needs. Let's add a new rule specifying this alias as the Validator to validate our project name attribute. We'll append it to the existing rules:

public function rules()
  {
    // NOTE: you should only define rules for those attributes that will receive user inputs.
    return array(
      array('create_user_id, update_user_id',   'numerical','integerOnly'=>true),
      array('name', 'length', 'max'=>128),
      array('create_time, update_time', 'safe'),
      // The following rule is used by search().
      // Please remove those attributes that should not be searched.
      array('id, name, description, create_time, create_user_id, 
update_time, update_user_id', 'safe', 'on'=>'search'),
      array('name', 'required'),
    );
  }

By saving this file and viewing the new Project form again at: http://localhost/ trackstar/index.php?r=project/create, we see a little red asterisk next to the Name field. This indicates that this field is now required. Try submitting the form without this field filled in. You should see an error message indicating that the Name field cannot be blank, as shown in the following screenshot:

While we are making these changes, let's go ahead and make the Description field required as well. All we have to do is add the Description field to the list of fields specified in the new rule we just added, as such:

array('name, description', 'required'),

So, we see we can specify multiple fields in the attribute list by comma separating them. With this in place, you will see that our form now indicates that both the name and the description are required. Attempting to submit either one without a value will result in a form validation error.

If we had stipulated the name and description columns as NOT NULL as part of the SQL when initially creating the table, then this rule would have been autogenerated for us when we created the model class using the Gii code generation tool. It will automatically add rules based on the definitions of the columns in the table. For example, columns with NOT NULL constraints will be added as required. As another example, columns that have length restrictions, like our name column being defined as varchar(128), will have character limit rules automatically applied. We notice by taking another look at our rules() method in the Project AR class that Gii auto created the rule

array('name', 'length', 'max'=>128)
for us based on its column definition.

Reading the project

Viewing the detail listing of our new project: http://localhost/trackstar/ index.php?r=project/view&id=4, does, basically, demonstrate the "R" in CRUD. However, to view the entire listing, we can click on the List Project link in the right column. This takes us back to where we started, except now we have our newly created project in the project list. So, we have the ability to retrieve a listing of all of the projects in the application, as well as view the details of each project individually.

Updating and deleting projects

Navigating back to a project details page can be done by clicking the little project ID link on any of the projects in the listing. Let's do this for our newly created project, which is ID: 4 in our case. Clicking this link takes us to the project details page for this project. This page has a number of action operations in the right-hand column, as the next screenshot shows:

We see both of the Update Project and Delete Project links which provide us with the "U" and "D" in our CRUD operations respectively. We'll leave it up to you to verify that these links do work as expected.

Managing projects in admin mode

The last link we have not covered in the previous screenshot depicting our project operations is the Manage Project link. Go ahead and click on this link. It will most likely result in an authorization error, as shown in the following screenshot:

The reason for this error is that when we had to log into the application in order to create a new project, we used demo/demo as our username/password combination. The code generated by Gii restricts the access to this functionality to administrators.

An administrator in this context is simply someone who has logged in with the username/password combination of admin/admin. Go ahead and log out of the application by clicking Logout (demo) from the main, top, navigation. Then log in again, but this time, use these administrator credentials. Once successfully logged in as admin (you can verify this by ensuring the logout link reads Logout (admin). Navigate back to a specific project listing page, for example: http://localhost/ trackstar/index.php?r=project/view&id=4, and try the Manage Projects link again .We should now see what is shown in the following screenshot:

What we now see is a highly interactive version of our project listing page. It displays all the projects in an interactive data table. Each row has inline links, to view, update and delete each project. Clicking on any of the column header links sorts the project list by that column value. The little input boxes in the second row allow you to search this project list by keywords within those individual column values. The Advanced Search link exposes an entire search form providing the ability to specify multiple search criteria, to submit against one search. The next screenshot displays this Advanced Search form:

Wow! We have basically implemented all of the functionality we set out to achieve in this iteration, and haven't really had to code much of anything. In fact, with the help of Gii, we have implemented basic project searching functionality that we were not expecting to achieve. Though basic, we have a fully functional application with features specific to a project task tracking application, and have done very little coding to achieve it.

But don't hit the beach just yet. All of this scaffolding code is not really intended to fully replace application development. Rather, it is there to help support us as we work to build the real application. As we work through all the details and nuances of how the project functionality should work, we can rely on this autogenerated code to keep things moving forward. We'll keep as much of it as we can, depending on project requirements as we move forward, but this type of autogenerated code scaffolding is not intended to be a complete solution for all the functionality we will need to manage the projects in our application.

More on testing—fixtures

Before we move on to adding more functionality into our TrackStar application, we need to briefly revisit our testing configuration. As we previously discussed, our unit tests actually added new projects to our application in our development environment. Also, even after we completed our tests by deleting the row we created, the database will reuse that same project identifier on subsequent inserts. So, as we continue to run our tests, we will notice gaps in our project ID sequence (which could be confusing during normal development).

The problem is that the unit tests are run against the same database that the web form uses when creating new projects. As a result, there is potential for some issues to arise. What we need to do is to configure our tests to run against a separate, mirrored database, that is dedicated just to testing. What we also need is a way to ensure that our tests are always run in the same manner, against the same data. The former is an easy change in a configuration file, which we will make shortly. The latter is achieved through the use of fixtures.

A test fixture is a system state or context in which tests are run. We want to run our tests a multiple number of times, and each time they run, we want to be able to have them return repeatable results. A fixture is intended to provide a well-known and fixed environment in which to run our tests. Typically, a fixture's job is to ensure that all of the objects involved in the testing are consistently initialized to a particular state. One typical example of a fixture is the loading of a database table with a fixed and known set of data.

Fixtures in yii are PHP files that return an array specifying the initial data configuration. They are typically named the same as the database table they represent, and are located under the protected/tests/fixtures/ folder. So, to specify project fixture data, we will need to create a new file in this directory called tbl_project.php. This file holds the fixed and known set of data that will initialize our Project database table before any tests in the /tests/unit/ProjectTest.php file are run. This fixture file is specified at the top of the ProjectTest.php test file:

class ProjectTest extends CDbTestCase 
{ 
  public $fixtures=array
    (
      'projects'=>'Project',
     );
}

Configuring the fixture manager

Setting up these types of database fixtures can be an extremely time consuming part of the testing process. Yii comes, once again, to rescue us from this tedium by the providing CdbFixtureManager class. When configured as an application component, it will provide the following functionality:

  • Before all tests are run, it resets all the relevant tables to a known data state
  • Before a single test is run, it can reset specified tables to a known data state
  • During the execution of a test, it provides access to the rows of data that are part of the fixed data state

To use the fixture manager, we configure it in the application configuration files. This was actually already done for us when we created the initial application. If you open up the application configuration file specific to testing, protected/config/test. php, you will see the following application component defined:

fixture'=>array( 
      'class'=>'system.test.CDbFixtureManager',
),

So the application has already been configured to use this fixture manager. Now we need to create a new fixture.

Creating a fixture

A fixture in Yii is implemented as a PHP file that returns an array representing the initial rows of data for a particular table. The filename is the same as the table name. By default, these fixture files are expected to be placed in the folder protected/tests/fixtures. You can use the CDbFixtureManager::basePath property in the application configuration to customize this location if desired. Let's provide an example by creating a new fixture for our tbl_project database table. Create a new file, protected/tests/fixtures/tbl_project.php, with the following contents:

<?php
return array(
  'project1'=>array(
    'name' => 'Test Project 1',
    'description' => 'This is test project 1',
    'create_time' => '',
    'create_user_id' => '',
    'update_time' => '',
    'update_user_id' => '',
  ),
  'project2'=>array(
    'name' => 'Test Project 2',
    'description' => 'This is test project 2',
    'create_time' => '',
    'create_user_id' => '',
    'update_time' => '',
    'update_user_id' => '',
  ),  
  'project3'=>array(
    'name' => 'Test Project 3',
    'description' => 'This is test project 3',
    'create_time' => '',
    'create_user_id' => '',
    'update_time' => '',   'update_user_id' => '',
  ),
);

As we can see, our fixture array has keys that represent entries in our table. The value of these keys are themselves arrays with a key=>value pair for each column in the table. We have added three rows, but you can add as many as you like. For simplicity, we have only filled out the values we have previously stipulated cannot be NULL, that is, the name and description fields. This will be enough data for us to demonstrate the use of fixtures.

You may have also noticed that the id column was not specified in the previous fixture data. This column is defined to be an auto-increment field. The value for this column will be handled by the database itself when we insert new rows.

Configuring this fixture for use

We still need to tell our unit tests to actually use this fixture we just created. We do this in the unit test file. In this case, we will need to add our fixture declaration to the top of our test file protected/tests/unit/ProjectTest.php as such:

<?php
class ProjectTest extends CDbTestCase
{
    public $fixtures=array
    (
        'projects'=>'Project',
    );
}

So, what we have done is specified the $fixtures member variable to be an array that specifies which fixtures will be used by this test. The array represents a mapping from fixture names that will be used in the tests to model class names or fixture table names (for example, from fixture name projects to model class Project). When using a model class name, as in this case, the underlying tables that correspond to the model class will be considered as fixture tables. As we described earlier, it is the fixture manager that will manage these underlying tables and reset the data to some known state each time a test method is executed.

If you need to use a fixture for a table that is not represented by an AR class, you need to prefix table name with a colon (for example, :tbl_project) to differentiate it from the model class name.

Fixture names allow us to access the fixture data in test methods in a convenient way. So, for example, now that we have defined this in our ProjectTest class, we can access our fixture data in the following ways:

// return all rows in the 'Project' fixture table
$projects = $this->projects;
// return the row whose alias is 'project1' in the `Project` fixture table
$projectOne = $this->projects['project1'];
// If our fixture is associated with an active record, return the AR instance representing
// the 'project1' fixture data row
$project = $this->projects('project1');

We'll provide more concrete examples when we change some of our actual unit tests to take advantage of this fixture data. First we need to make another change to our testing environment.

Specifying a test database

As we previously mentioned, we need to separate our development database from our testing database so that our testing will not continue to interfere with our development.

The test specific application configuration file provides a place for us to do just that. We need to create another new database, call it trackstar_test. We also need to replicate the schema we have in our current trackstar_dev database. This is easy as we just have the one tbl_project table at the moment. Please proceed as you did in Chapter 4 to create this new database with the tbl_project table. Once created, we can add the database connection information as an application component to our test specific configuration file located at protected/config/test.php. You can copy the db component from your main.php config file that we added back in Chapter 4. For MySQL users, like us, we add the following highlighted code to our test config file:

return CMap::mergeArray(
  require(dirname(__FILE__).'/main.php'),
  array(
    'components'=>array(
      'fixture'=>array(
        'class'=>'system.test.CDbFixtureManager',
      ),
      
      'db'=>array(
        'connectionString' =>  'mysql:host=localhost;dbname=trackstar_test',
        'emulatePrepare' => true,
        'username' => '[your db username]',
        'password' => '[your db password]',
        'charset' => 'utf8',
         ),
      
    ),
  )
);

When we run our tests, this test config is loaded, rather than the main config file. This file actually merges the array from the main config file with the array defined in this test config file. If the same components or config values are defined in both, the values in the test file will take precedence. Now when we run our unit tests, we will be manipulating this test database rather than our development one, and won't run the risk of having our test suite negatively impact our development progress.

Using fixtures

Now that we have adjusted our test environment to use a separate database, we should take advantage of what fixtures have to offer. When we initially wrote the unit tests for the CRUD operations against the Project AR class, we put all of the Create, Read, Update and Delete tests all into one test method we called testCRUD(). This has the disadvantage of lumping all these discrete tests into one big test. If the first create fails, then the execution of that entire test method stops, and the tests for Read, Update and Delete are never even run. Ideally, we should separate these so that one test does not have to depend on the others. The main reason we wrote the test this way was to avoid the need to ensure the order in which the test methods must run. If we separated the create test from the read test, there is a potential for the read method to be executed prior to the create method, which would result in a failed test, as no rows would have been created to read back. However, we can avoid this issue if we use the fixture data.

Now that our new test environment configured to use a new dedicated database, and our fixture data defined, we can decouple our CRUD unit tests. This will give us some concrete examples of how to use our fixture data.

Let's start with Read. Open up the ProjectTest.php unit test file and add the following test method:

public function testRead()
{
    $retrievedProject = $this->projects('project1');
    $this->assertTrue($retrievedProject instanceof Project);
    $this->assertEquals('Test Project 1',$retrievedProject->name);
}

We know that before this test is run, the fixture manger will reset the tbl_project table, in the trackstar_test database, to the known state defined by the fixture data. Here, we are simply reading back the first row of data, referencing the row alias, project1, which returns a Project AR instance based on that first row of data defined in our protected/tests/fixtures/tbl_project.php fixture file. We then test that the returned entity is an instance of Project and test to make sure its name is what we established in the fixture data.

We can similarly add separate testCreate(), testUpdate(), and testDelete() methods. The entire test file after making the needed change to decouple all of these CRUD tests into separate methods is shown below:

<?php 
class ProjectTest extends CDbTestCase 
{
    public $fixtures=array( 
      'projects'=>'Project', 
    ); 
 
    public function testCreate() 
    { 
        //CREATE a new Project 
       $newProject=new Project; 
       $newProjectName = 'Test Project Creation'; 
       $newProject->setAttributes(array( 
           'name' => $newProjectName, 
           'description' => 'This is a test for new project creation', 
           'createTime' => '2009-09-09 00:00:00', 
           'createUser' => '1', 
           'updateTime' => '2009-09-09 00:00:00', 
           'updateUser' => '1', 
       ));
 
       $this->assertTrue($newProject->save(false)); 
 
       //READ back the newly created Project to ensure the creation worked
       $retrievedProject=Project::model()->findByPk($newProject->id);
       $this->assertTrue($retrievedProject instanceof Project);
       $this->assertEquals($newProjectName,$retrievedProject->name);
    }
 
    public function testRead() 
    { 
       $retrievedProject = $this->projects('project1'); 
       $this->assertTrue($retrievedProject instanceof Project); 
       $this->assertEquals('Test Project 1',$retrievedProject->name); 
    }
 
    public function testUpdate() 
    {
       $project = $this->projects('project2');
       $updatedProjectName = 'Updated Test Project 2';
       $project->name = $updatedProjectName;
       $this->assertTrue($project->save(false)); 
       //read back the record again to ensure the update worked
       $updatedProject=Project::model()->findByPk($project->id);
       $this->assertTrue($updatedProject instanceof Project);
       $this->assertEquals($updatedProjectName,$updatedProject->name);
    }
 
    public function testDelete()
    { 
       $project = $this->projects('project2'); 
       $savedProjectId = $project->id; 
       $this->assertTrue($project->delete()); 
       $deletedProject=Project::model()->findByPk($savedProjectId); 
       $this->assertEquals(NULL,$deletedProject); 
    }
}

Now, if any one of these fails when we run the tests, the rest will still execute providing us more granular feedback on each distinct operation.

Summary

Even though we did not do a ton of actual coding in this chapter, we accomplished quite a lot. We created a new database table, which allowed us to see Yii AR in action. We used the Gii code generation tool to first create an AR class to wrap our tbl_project database table. We then wrote tests to try out this new class and got a lot of exposure to using these AR class types.

We then demonstrated how to use the Gii code generation tool to generate actual CRUD functionality in the Web application. With this amazing tool, we achieved most of application functionality that we outlined for this iteration. We made one small change to enforce the project name and description on form submission, which showcased the form validation functionality.

Finally, we introduced testing fixtures in Yii, and made some adjustments to our testing environment to take advantage of this feature.

In the next iteration, we will build on what we have learned here and dive more deeply into Active Record in Yii as we introduce related entities in our data model.

评论 X

      友荐云推荐
      Copyright 2011-2014. YiiBook.com