One of the best ways to get started with the Yii framework is by making useful applications. The first application that will be covered in this book is a simple task management application. In this chapter, we will cover the planning of the development of this project, developing the application, and creating useful components that we will reuse in later chapters.
One of the most important steps in starting a new project is planning it. By planning the project before we begin programming, we can easily identify most (if not all) models that our application will use, key features that we'll need to implement, as well as any areas that may cause us problems while developing our applications. Breaking down the project beforehand also helps us estimate how long it will take to develop each part of our applications as well as the application as a whole. While requirements and expectations for our application will most likely change during its development, identifying the core components of your application will help ensure that the core functionality of our application works as we intend.
For our task management application, there are two main components: tasks and projects. Let's break each of these components down.
The first component of our application is tasks. A task is an item that needs to be done by our user and usually consists of a brief, concise title, and a description of what needs to be done to complete that task. Sometimes, a task has a due date or time associated with it that lets us know when the task needs to be completed. Tasks also need to indicate whether they have been completed or not. Finally, a task is usually associated with a group or project that contains similar or related tasks.
The second component of our application is projects. Projects group related tasks together and usually have a descriptive name associated with them. Projects may also have a due date or time associated with them, which indicates when all tasks in a project need to be completed. We also need to be able to indicate whether or not a project is completed.
By breaking down our project, we've also identified a third component of our application: users. Users in our application will have the ability to create and manage both projects and tasks as well as view the statuses and due dates of any given task. While this component of our application may seem obvious, identifying it early on allows us to better understand the interaction that our users will have with the various components of our application.
With the core components of our application identified, we can now begin to think about what our database is going to look like. Let's start with the two database tables.
By looking at our requirements, we can identify several columns and data types for our tasks
table. As a rule, each task that we create will have a unique, incrementing ID associated with it. Other columns that we can quickly identify are the task name, the task description, the due date, and whether or not the task has been completed. We also know that each task is going to be associated with a project, which means we need to reference that project in our table.
There are also some columns we can identify that are not so obvious. The two most useful columns that aren't explicitly identified are timestamps for the creation date of the task and the date it was last updated on. By adding these two columns, we can gain useful insights into the use of our application. It's possible that in the future, our imaginary client may want to know how long an unresolved task has been open for and whether or not it needs additional attention if it has not been updated in several days.
With all the columns and data types for our table identified, our tasks
table written with generic SQL data types will look as follows:
ID INTEGER PRIMARY KEY name TEXT description TEXT completed BOOLEAN project_id INTEGER due_date TIMESTAMP created TIMESTAMP updated TIMESTAMP
By looking at our requirements for projects, we can easily pick out the major columns for our projects
table: a descriptive name, whether or not the project has been completed, and when the project is due. We also know from our tasks
table that each project will need to have its own unique ID for the task to reference. When the time comes to create our models in our application, we'll clearly define the one-to-many relationship between any given project and the many tasks belonging to it. If we keep a created and updated column, our projects
table written in generic SQL will look as follows:
ID INTEGER PRIMARY KEY name TEXT completed BOOLEAN due_date TIMESTAMP created TIMESTAMP updated TIMESTAMP
Tip
Downloading the example code
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Our application requirements also show us that we need to store users somewhere. For this application, we're going to store our users in a flat file database. In Chapter 3, Scheduled Reminders, we will expand upon this and store users in their own database table.
Now that we have decided what our database is going to look like, it's time to start thinking about where we're going to store this information. To help familiarize yourself with the different database adapters Yii natively supports, for this project, we will be using SQLite. Since we now know where we're going to store our data, we can identify all the correct data types for database tables.
Since SQLite only supports five basic data types (NULL
, INTEGER
, REAL
, TEXT
, and BLOB
), we need to convert a few of the data types we initially identified for this table into ones that SQLite supports. Since SQLite does not support Boolean or timestamps natively, we need to find another way of representing this data using a data type that SQLite supports. We can represent a Boolean value as an integer either as 0 (false) or 1 (true). We can also represent all of our timestamp columns as integers by converting the current date to a Unix timestamp.
With our final data types figured out, our tasks
table now will look like this:
ID INTEGER PRIMARY KEY name TEXT description TEXT completed INTEGER project_id INTEGER due_date INTEGER created INTEGER updated INTEGER
By spending a few minutes thinking about our application beforehand, we've successfully identified all the tables for our application, how they interact with one another, and all the column names and data types that our application will be using. We've done a lot of work on our application already without even writing a single line of code. By doing this work upfront, we have also reduced some of the work we'll need to do later on when creating our models.
With our final database structure figured out, we can now start writing code. Using the instructions in the official guide (http://www.yiiframework.com/doc/guide/), download and install the Yii framework. Once Yii is installed, navigate to your webroot
directory, and create a new folder called tasks
. Next, navigate inside the tasks
folder, and create the following folder structure to serve as our application's skeleton:
tasks/ assets/ protected/ commands/ components/ config/ controllers/ data/ migrations/ models/ runtime/ views/ layouts/ projects/ tasks/ site/
Tip
Yii has a built-in tool called yiic
, which can automatically generate a skeleton project. Refer to the quick start guide (http://www.yiiframework.com/doc/guide/1.1/en/quickstart.first-app) for more details.
Depending upon the web server you are using, you may also need to create a .htaccess
file in the root directory of your tasks
folder. Information about how to set up your application for the web server you are using can be found in the quick start guide (http://www.yiiframework.com/doc/guide/1.1/en/quickstart.apache-nginx-config).
After setting up our skeleton structure, we can first create our configuration file located at protected/config/main.php
. Our configuration file is one of the most important files of our application as it provides Yii with all the critical information necessary to load and configure our application. The configuration file informs Yii about the files to be preloaded by Yii's built-in autoloader, the modules to be loaded, the component to be registered, and any other configuration options we want to pass to our application.
For this application, we will be enabling the Gii module, which will allow us to create models based upon our database structure. We will also enable two components, urlManager
and db
, which will allow us to set up custom routes and access our SQLite database. Have a look at the following code snippet:
<?php return array( 'basePath'=>dirname(__FILE__).DIRECTORY_SEPARATOR.'..', 'name'=>'Task Application', 'import'=>array( 'application.models.*', 'application.components.*', ), 'modules'=>array( // Include the Gii Module so that we can //generate models and controllers for our application 'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>false, 'ipFilters'=>false ), ), 'components'=>array( 'urlManager'=>array( 'urlFormat'=>'path', 'showScriptName'=>false, 'rules'=>array( '<controller:\w+>/<id:\d+>'=>'<controller>/view', '<controller:\w+>/<action:\w+>/<id:\d+>'=>'<controller>/<action>', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>', ), ), // Define where our SQLite database is going to be // stored, relative to our config file 'db'=>array( 'connectionString' => 'sqlite:'.dirname(__FILE__).'/../data/tasks.db', ) ) );
Next, we can create our index.php
file as follows, which will serve as our bootstrap endpoint for our web application:
<?php // change the following paths if necessary $yii='/opt/frameworks/php/yii/framework/yii.php'; $config=dirname(__FILE__).'/protected/config/main.php'; // remove the following lines when in production mode defined('YII_DEBUG') or define('YII_DEBUG',true); // specify how many levels of call stack should be shown in each log message defined('YII_TRACE_LEVEL') or define('YII_TRACE_LEVEL',3); require_once($yii); Yii::createWebApplication($config)->run();
Finally, we can create our applications yiic
file in protected/yiic.php
as follows, which will allow us to run console commands native to Yii from our application:
<?php // change the following paths if necessary $config=dirname(__FILE__).'/config/main.php'; $config = require($config); require_once('/opt/frameworks/php/yii/framework/yiic.php');
Now that our application can be bootstrapped, we can create our database. To do this, we are going to create a migration. Migrations are a feature of Yii that allow the creation and modification of your database to be a part of your application. Rather than creating schema modifications in pure SQL, we can use migrations to grow our database as a part of our application. In addition to acting as a revision system for our database schema, migrations also have the added benefit of allowing us to transmit our database with our application without having to worry about sharing data that would be stored in our database.
To create our database, open up your command-line interface of choice, navigate to your tasks directory, and run the following command:
$ php protected/yiic.php migrate create tasks
The yiic
command will then prompt you to confirm the creation of the new migration:
Yii Migration Tool v1.0 (based on Yii v1.1.14) Create new migration '/var/www/tasks/protected/migrations/m131213_013354_tasks.php'? (yes|no) [no]:yes New migration created successfully.
Tip
To prevent naming conflicts with migrations, yiic
will create the migration with the following naming structure: m<timestamp>_<name>
. This has the added benefit of allowing us to sequentially apply or remove specific migrations based upon the order in which they were added. The exact name of your migration will be slightly different than the one listed in the preceding command.
After confirming the creation of the migration, a new file will be created in the protected/migrations
folder of our application. Open up the file, and add the following to the up
method:
$this->createTable('tasks', array( 'id' => 'INTEGER PRIMARY KEY', 'title' => 'TEXT', 'data' => 'TEXT', 'project_id' => 'INTEGER', 'completed' => 'INTEGER', 'due_date' => 'INTEGER', 'created' => 'INTEGER', 'updated' => 'INTEGER' )); $this->createTable('projects', array( 'id' => 'INTEGER PRIMARY KEY', 'name' => 'TEXT', 'completed' => 'INTEGER', 'due_date' => 'INTEGER', 'created' => 'INTEGER', 'updated' => 'INTEGER' ));
Notice that our database structure matches the schema that we identified earlier in the chapter.
Next, replace the contents of the down
method with instructions to drop the database table if we call migrate down
from the yiic
command. Have a look at the following code:
$this->dropTable('projects'); $this->dropTable('tasks');
Now that the migration has been created, run migrate up
from the command line to create the database and apply our migration. Run the following commands:
$ php protected/yiic.php migrate up Yii Migration Tool v1.0 (based on Yii v1.1.14) Total 1 new migration to be applied: m131213_013354_tasks Apply the above migration? (yes|no) [no]:yes *** applying m131213_013354_tasks *** applied m131213_013354_tasks (time: 0.009s) Migrated up successfully.
Now, if you navigate to protected/data/
, you will see a new file called tasks.db
, the SQLite database that was created by our migrations.
Now that our database has been created, we can create models for our database table. To create our models, we are going to use Gii, Yii's built-in code generator.
Open up your web browser and navigate to http://localhost/gii
(in this book, we will always use localhost
as our working hostname for our working project. If you are using a different hostname, replace localhost
with your own). Once loaded, you should see the Yii Code Generator, as shown in the following screenshot:
Tip
If you aren't able to access Gii, verify that your web server has rewriting enabled. Information about how to properly configure your web server for Yii can be found at (http://www.yiiframework.com/doc/guide/1.1/en/quickstart.apache-nginx-config).
Click on the link titled Model Generator, and then fill in the form on the page that appears. The table name should be set to tasks
. The model name should prepopulate. If it doesn't, set the model name to Tasks
, and then click on preview. Once the page has reloaded, you can preview what the model will look like before clicking on the Generate button to write your new model to your protected/models/
directory. Once you have generated your model for tasks
, repeat the process for projects
.
Now that our models have been created, there are several sections that should be modified.
The first part of our model that needs to be modified is the validation rules. Validation rules in Yii are stored in the model's rules()
method and are executed when the model's validate()
method is called. Starting with our tasks
model, we can see that Gii has already prepopulated our validation rules for us based upon our database.
There are several fields of this model that we would like to always have set, namely, project_id
, title
, the task itself, and whether or not it has been completed. We can make these fields required in our model by adding a new array to our rules section, as follows:
array('project_id, title, data, completed', 'required')
By making these fields required in our model, we can make client- and server-side validation easier when we start making forms. Our final method for this model will look as follows:
public function rules() { return array( array('project_id, completed, due_date, created, updated', 'numerical', 'integerOnly'=>true), array('project_id, title, data, completed', 'required'), array('title, data', 'safe'), array('id, title, data, project_id, completed, due_date, created, updated', 'safe', 'on'=>'search'), ); }
Our project's models should also be changed so that the project name and its completed status are required. We can accomplish this by adding the following to our validation rules array:
array('name, completed', 'required')
Tip
Additional validation rules can be found in the Yii wiki at http://www.yiiframework.com/wiki/56/
Another component of our model that we should change is the relations()
method. By declaring model relations in Yii, we can take advantage of the ability of ActiveRecords to automatically join several related models together and retrieve data from them without having to explicitly call that model for its data.
For example, once our model relations are set up, we will be able to retrieve the project name from the Tasks model, as follows:
Tasks::model()->findByPk($id)->project->name;
Before we can declare our relations though, we need to determine what the relations actually are. Since SQLite does not support foreign key relations, Gii was unable to automatically determine the relations for us.
In Yii, there are four types of relations: BELONGS_TO
, HAS_MANY
, HAS_ONE
, and MANY_MANY
. Determining the relation type can be done by looking at the foreign key for a table and asking which relational type fits best based upon the data that the table will store. For this application, this question can be answered as follows:
Tasks belong to a single project
A project has one or many tasks
Now that we have determined our relationship types between our two tables, we can write the relations. Starting with the tasks
table, replace the relations()
method with the following:
public function relations() { return array( 'tasks' => array(self::HAS_MANY, 'Task', 'project_id') ); }
The syntax for the relations array is as follows:
'var_name'=>array('relationship_type', 'foreign_model', 'foreign_key', [... other options ..])
For our projects model, our relations()
method looks like this:
public function relations() { return array( 'tasks' => array(self::HAS_MANY, 'Tasks', 'project_id') ); }
In our model's current state, whenever a project is deleted, all the tasks associated with it become orphaned. One way of dealing with this edge case is to simply delete any tasks associated with the project. Rather than writing code to handle this in the controller, we can have the model take care of it for us by referencing the project's model's beforeDelete()
method as follows:
public function beforeDelete() { Tasks::model()->deleteAllByAttributes(array('project_id' => $this->id)); return parent::beforeDelete(); }
There is also metadata about a project that we cannot obtain directly from the projects
database table. This data includes the number of tasks a project has, as well as the number of completed tasks a project has. We can obtain this from our model by creating two new methods in the project's model, as follows:
public function getNumberOfTasks() { return Tasks::model()->countByAttributes(array('project_id' => $this->id)); } public function getNumberOfCompletedTasks() { return Tasks::model()->countByAttributes(array('project_id' => $this->id, 'completed' => 1)); }
Additionally, we can determine the progress of a project by getting a percentage of completed tasks versus the total number of tasks, as follows:
public function getPercentComplete() { $numberOfTasks = $this->getNumberOfTasks(); $numberOfCompletedTasks = $this->getNumberOfCompletedTasks(); if ($numberOfTasks == 0) return 100; return ($numberOfCompletedTasks / $numberOfTasks) * 100; }
The last change needed to be made to the models is to enable them to automatically set the created and updated timestamp in the database every time the model is saved. By moving this logic into the models, we can avoid having to manage it either in the forms that submit the data or in the controllers that will process this data. This change can be made by adding the following to both models:
public function beforeSave() { if ($this->isNewRecord) $this->created = time(); $this->updated = time(); return parent::beforeSave(); }
In the beforeSave()
method, the updated property is always set every time the model is saved, and the created property is only set if ActiveRecord considers this to be a new record. This is accomplished by checking the isNewRecord
property of the model. Additionally, both properties are set to time()
, the PHP function used to get the current Unix timestamp.
The last piece of code that is important in this method is return parent::beforeSave();
. When Yii's save()
method is called, it checks that beforeSave()
returns true before saving the data to the database. While we could have this method return true, it's easier to have it return whatever the parent model (in this case CActiveRecord
) returns. It also ensures that any changes made to the parent model will get carried to the model.
Tip
Since the beforeSave()
method is identical for both models, we could also create a new model that only extended CActiveRecord
and only implemented this method. The tasks and projects model will then extend that model rather than CActiveRecord
and will inherit this functionality. Moving shared functionality to a shared location reduces the number of places where code needs to be written and, consequently, the number of places a bug can show up in.
Up until this point, all the code that has been written is backend code that the end user won't be able to see. In this section, we will be creating the presentation layer of our application. The presentation layer of our application is composed of three components: controllers, layouts, and views. For this next section, we'll be creating all the three components.
As a developer, we have several options to create the presentation layer. One way we can create the presentation layer is using Gii. Gii has several built-in tools that can assist you in creating new controllers, forms for our views, and even full create, read, update, and delete (CRUD) skeletons for our application. Alternatively, we can write everything by hand.
The first part of the presentation layer we are going to work on is the projects section. To begin with, create a new file in protected/controllers/
called ProjectControllerProjectController.php
that has the following class signature:
<?php class ProjectControllerProjectController extends CController {}
For our controllers, we will be extending Yii's base class called CController
. In future chapters, we will create our own controllers and extend the controllers from them.
Before we can start displaying content from our new action, we'll need to create a layout for our content to be rendered in. To specify our layout, create a public property called $layout
, and set the value to 'main'
:
public $layout = 'main';
Next, let's create our first action to make sure everything is working:
public function actionIndex() { echo "Hello!"; }
Now, we should be able to visit http://localhost/projects/index
from our web browser and see the text Hello
printed on the screen. Before we continue defining our actions, let's create a layout that will help our application look a little better.
The layout that we specified references the file located in protected/views/layouts/main.php
. Create this file and open it for editing. Then, add the following basic HTML5 markup:
<!DOCTYPE html> <html> <head> </head> <body> </body> </html>
Then add a title within the <head>
tag that will display the application name we defined in protected/config/main.php
:
<title><?php echo Yii::app()->name; ?></title>
Next, let's add a few meta tags, CSS, and scripts. To reduce the number of files we need to download, we'll be including styles and scripts from a publicly available Content Distribution Network (CDN). Rather than writing markup for these elements, we're going to use CClientScript
, a class made to manage JavaScript, CSS, and meta tags for views.
For this application, we'll be using a frontend framework called Twitter Bootstrap. This framework will style many of the common HTML tags that our application will use, providing it with a cleaner overall look.
Tip
When you're ready to go live with your application, you should consider moving the static assets you are using to a CDN, referencing popular libraries such as Twitter Bootstrap and jQuery from a publicly available CDN. CDNs can help to reduce hosting costs by reducing the amount of bandwidth your server needs to use to send files. Using a CDN can also speed up your site since they usually have servers geographically closer to your users than your main server.
First, we're going to call CClientScript
, as follows:
<?php $cs = Yii::app()->clientScript; ?>
Secondly, we're going to set the Content-Type
to text/html
with a UTF-8
character set, as follows:
<?php $cs->registerMetaTag('text/html; charset=UTF-8', 'Content-Type'); ?>
Next, we're going to register the CSS from Twitter Bootstrap 3 from a popular CDN, as follows:
<?php $cs->registerCssFile( '//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css' ); ?>
Then we'll register the JavaScript library for Twitter Bootstrap:
<?php $cs->registerScriptFile( '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js' ); ?>
Finally, we're going to register jQuery 2.0 and have Yii placed at the end of the <body>
tag, as follows:
<?php $cs->registerScriptFile( '//code.jquery.com/jquery.js', CClientScript::POS_END ); ?>
CClientScript
also supports method chaining, so you could also change the preceding code to the following:
<?php Yii::app()->clientScript ->registerMetaTag('text/html; charset=UTF-8', 'Content-Type') ->registerCssFile( '//netdna.bootstrapcdn.com/bootstrap/3.0.3/css/bootstrap.min.css' ->registerScriptFile( '//netdna.bootstrapcdn.com/bootstrap/3.0.3/js/bootstrap.min.js' ) ->registerScriptFile( 'https://code.jquery.com/jquery.js' , CClientScript::POS_END); ?>
For the last part of our layout, let's add a basic header within our <body>
tag that will help with navigation, as follows:
<div class="row"> <div class="container"> <nav class="navbar navbar-default navbar-fixed-top" role="navigation"> <div class="navbar-header"> <a class="navbar-brand" href="/"><?php echo CHtml::encode(Yii::app()->name); ?></a> </div> </nav> </div> </div>
After the closing </div>
tag, add the following:
<div class="row" style="margin-top: 100px;"> <div class="container"> <?php echo $content; ?> </div> </div>
The $content
variable that we've added to our layout is a special variable that contains all the rendered HTML markup from our view files and is defined by the CController
class in the
render()
method. Yii will automatically populate this variable for us whenever we call the render()
method from within our controllers.
With our layout defined, we can get back to creating actions. Let's start by modifying our actionIndex()
method so that it renders a view.
First, create a variable to store a searchable copy of our model. Have a look at the following code:
$model = new Projects('search');
Next, render a view called index
, which references protected/views/projects/index.php
, and pass the model we created to this view, as follows:
$this->render('index', array('model' => $model));
Now, create the view file in protected/views/projects/index.php
and open it for editing. Begin by adding a button in the view as follows, which will reference the save
action that we will create later on:
<?php echo CHtml::link('Create New Project', $this->createUrl('/projects/save'), array('class' => 'btn btn-primary pull-right')); ?> <div class="clearfix"></div>
Then add a descriptive title so that we know what page we are on. Have a look at the following line of code:
<h1>Projects</h1>
Finally, create a new widget that uses CListView
, a built-in Yii widget designed for displaying data from CActiveDataProvider
. In Yii, widgets are frontend components that help us to quickly generate commonly used code, typically for presentation purposes. This widget will automatically generate pagination for us as necessary and will allow each of our items to look the same. Have a look at the following code:
<?php $this->widget('zii.widgets.CListView', array( 'dataProvider'=>$model->search(), 'itemView'=>'_project', )); ?>
The new widget that we created consists of two parts. The first is the dataProvider
, which provides data to the widget. This data comes from our project's model's search()
method, a piece of code automatically generated by Gii.
The second part of the widget is the itemView
, which references the specific view file that our items will be rendered out of. In this case, the view references a file in the same directory of protected/views/projects
called _project.php
. Create this file and then add the following code to it:
<div> <div class="pull-left"> <p><strong><?php echo CHtml::link(CHtml::encode($data->name), $this->createUrl('/projects/tasks', array('id' => $data->id))); ?></strong></p> <p>Due on <?php echo date('m/d/Y', $data->due_date); ?></p> <?php if ($data->completed): ?> Completed <?php else: ?> <?php if ($data->numberOfTasks == 0): ?> <p>No Tasks</p> <?php else: ?> <p><?php echo $data->getPercentComplete(); ?>% Completed</p> <?php endif; ?> <?php endif; ?> </div> <div class="pull-right"> <?php echo CHtml::link(NULL, $this->createUrl('/projects/save', array('id' => $data->id)), array('title' => 'edit', 'class' => 'glyphicon glyphicon-pencil')); ?> <?php echo CHtml::link(NULL, $this->createUrl('/projects/complete', array('id' => $data->id)), array('title' => $data->completed == 1 ? 'uncomplete' : 'complete', 'class' => 'glyphicon glyphicon-check')); ?> <?php echo CHtml::link(NULL, $this->createUrl('/projects/delete', array('id' => $data->id)), array('title' => 'delete', 'class' => 'glyphicon glyphicon-remove')); ?> </div> <div class="clearfix"></div> </div> <hr/>
If we refresh our browser page now, our view will show us that no results were found. Before we can see data, we need to create an action and view to create and update it. Before we start creating new records, let's create two other actions that we outlined in our item's view: complete and delete.
First, let's create an action to mark a project as completed or uncompleted. This action will only be responsible for changing the completed field of the projects table to 0 or 1, depending on its current state. For simplicity, we can just XOR the field by 1 and save the model. Have a look at the following code:
public function actionComplete($id) { $model = $this->loadModel($id); $model->completed ^= 1; $model->save(); $this->redirect($this->createUrl('/projects')); }
Additionally, we'll create another private method called loadModel()
, which will load our appropriate model for us and throw an error if it cannot be found. For this method, we'll use CHttpException
, which will create an HTTP exception with the error message we provide if a model with the specified ID cannot be found. Have a look at the following code:
private function loadModel($id) { $model = Projects::model()->findByPk($id); if ($model == NULL) throw new CHttpException('404', 'No model with that ID could be found.'); return $model; }
Next, we'll create a method to delete the project. This method will use the loadModel()
method we defined earlier. Additionally, if we encounter an error deleting the model, we'll throw an HTTP exception so that the user knows something went wrong. Here's how we go about it:
public function actionDelete($id) { $model = $this->loadModel($id); if ($model->delete()) $this->redirect($this->createUrl('/projects')); throw new CHttpException('500', 'There was an error deleting the model.'); }
With the two other methods defined, we can now work on creating and updating a project. Rather than creating two actions to handle both these tasks, we're going to create one action that knows how to handle both by checking the ID that we'll pass as a GET
parameter. We can do that by defining a new action that looks as follows:
public function actionSave($id=NULL) {
We can then either create a new project or update a project based upon whether or not we were provided with an ID by the user. By taking advantage of loadModel()
, we also take care of any errors that would occur if an ID was provided but a project with that ID didn't exist. Have a look at the following code:
if ($id == NULL) $model = new Projects; else $model = $this->loadModel($id);
Next, we can detect whether the user submitted data by checking the $_POST
variable for an array called Projects
. If that array is defined, we'll assign it to our $model->attributes
object. Before saving the model, however, we'll want to convert whatever the user entered into a Unix timestamp. Have a look at the following code:
if (isset($_POST['Projects'])) { $model->attributes = $_POST['Projects']; $model->due_date = strtotime($_POST['Projects']['due_date']); $model->save(); }
Finally, we'll render the view and pass the model down to it, as follows:
$this->render('save', array('model' => $model));
Create a new file in protected/views/projects/
called save.php
and open it to edit. Begin by adding a header that will let us know whether we are editing a project or creating a new one, as follows:
<h1><?php echo $model->isNewRecord ? 'Create New' : 'Update'; ?> Project</h1>
Next, we'll create a new widget with CActiveForm
, which will take care of the hard tasks of creating and inserting form fields into our view file (such as what the names and IDs of form fields should be):
<?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'project-form', 'htmlOptions' => array( 'class' => 'form-horizontal', 'role' => 'form' ) )); ?> <?php $this->endWidget(); ?>
Between the beginWidget
and endWidget
call, add an error summary if the user encounters an error:
<?php echo $form->errorSummary($model); ?>
Then, after the error summary, add the form fields and their associated styles, as follows:
<div class="form-group"> <?php echo $form->labelEx($model,'name', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->textField($model,'name', array('class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'completed', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->dropDownList($model,'completed', array('0' => 'No','1' => 'Yes'), array('class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'due_date', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <div class="input-append date"> MM/DD/YYYY <?php $this->widget('zii.widgets.jui.CJuiDatePicker', array( 'model' => $model, 'attribute' => 'due_date', 'htmlOptions' => array( 'size' => '10', 'maxlength' => '10', 'class' => 'form-control', 'value' => $model->due_date == "" ? "" : date("m/d/Y", $model->due_date) ), )); ?> </div> </div> </div> <div class="row buttons"> <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save', array('class' => 'btn btn-primary pull-right')); ?> </div>
Note
Did you notice how we're taking advantage of the Yii widget called CJuiDatePicker
? This widget will provide us with a clean interface for selecting dates from a calendar view, rather than requiring our end user to type in the date manually and in the specified format we've requested.
Now we can create, update, view, and delete projects. Additionally, we've created an easy action to mark them as completed. Before we're done with this controller, we need to add an action that allows us to view tasks in our project.
Our tasks
action for this controller will function in the same manner as our index
action but will instead use a view called tasks
:
public function actionTasks($id=NULL) { if ($id == NULL) throw new CHttpException(400, 'Missing ID'); $project = $this->loadModel($id); if ($project === NULL) throw new CHttpException(400, 'No project with that ID exists'); $model = new Tasks('search'); $model->attributes = array('project_id' => $id); $this->render('tasks', array('model' => $model, 'project' => $project)); }
The tasks.php
view in protected/views/projects/tasks.php
will look as follows:
<?php echo CHtml::link('Create New Task', $this->createUrl('/tasks/save?Tasks[project_id]=' . $project->id), array('class' => 'btn btn-primary pull-right')); ?> <div class="clearfix"></div> <h1>View Tasks for Project: <?php echo $project->name; ?></h1> <?php $this->widget('zii.widgets.CListView', array( 'dataProvider'=>$model->search(), 'itemView'=>'_tasks', )); ?>
The _tasks.php
item view in protected/views/projects/tasks.php
will look as follows:
<div> <div class="pull-left"> <p><strong><?php echo CHtml::link(CHtml::encode($data->title), $this->createUrl('/tasks/save', array('id' => $data->id))); ?></strong></p> <p>Due on <?php echo date('m/d/Y', $data->due_date); ?></p> </div> <div class="pull-right"> <?php echo CHtml::link(NULL, $this->createUrl('/tasks/save', array('id' => $data->id)), array('class' => 'glyphicon glyphicon-pencil')); ?> <?php echo CHtml::link(NULL, $this->createUrl('/tasks/complete', array('id' => $data->id)), array('title' => $data->completed == 1 ? 'uncomplete' : 'complete', 'class' => 'glyphicon glyphicon-check')); ?> <?php echo CHtml::link(NULL, $this->createUrl('/tasks/delete', array('id' => $data->id)), array('class' => 'glyphicon glyphicon-remove')); ?> </div> <div class="clearfix"></div> </div> <hr/>
Now that we can manage projects, let's work on managing tasks. Our TasksController
is going to be nearly identical to our project's controller with only a few differences. Start by creating a new file in protected/controllers
called TasksController.php
that has the following signature:
<?php class TasksController extends CController {}
By only making a small change to our loadModel()
method, we can reuse the delete and complete action from our projects controller, as follows:
private function loadModel($id) { $model = Tasks::model()->findByPk($id); if ($model == NULL) throw new CHttpException('404', 'No model with that ID could be found.'); return $model; }
Our save
action is almost identical to our project's save
action. Have a look at the following code:
public function actionSave($id=NULL) { if ($id == NULL) $model = new Tasks; else $model = $this->loadModel($id); if (isset($_GET['Tasks'])) $model->attributes = $_GET['Tasks']; if (isset($_POST['Tasks'])) { $model->attributes = $_POST['Tasks']; $model->due_date = strtotime($_POST['Tasks']['due_date']); $model->save(); } $this->render('save', array('model' => $model)); }
The view file for this action is almost the same as well. If you haven't already, create a file called save.php
in protected/views/tasks/
, and then add the following lines of code to finish the view:
<ol class="breadcrumb"> <li><?php echo CHtml::link('Project', $this->createUrl('/projects')); ?></li> <li class="active"><?php echo $model->isNewRecord ? 'Create New' : 'Update'; ?> Task</li> </ol> <hr /> <h1><?php echo $model->isNewRecord ? 'Create New' : 'Update'; ?> Task</h1> <?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'project-form', 'htmlOptions' => array( 'class' => 'form-horizontal', 'role' => 'form' ) )); ?> <?php echo $form->errorSummary($model); ?> <div class="form-group"> <?php echo $form->labelEx($model,'title', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->textField($model,'title', array('class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'data', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->textArea($model,'data', array('class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'project_id', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->dropDownList($model,'project_id', CHtml::listData(Projects::model()->findAll(), 'id', 'name'), array('empty'=>'Select Project', 'class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'completed', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <?php echo $form->dropDownList($model,'completed', array('0' => 'No','1' => 'Yes'), array('class' => 'form-control')); ?> </div> </div> <div class="form-group"> <?php echo $form->labelEx($model,'due_date', array('class' => 'col-sm-2 control-label')); ?> <div class="col-sm-10"> <div class="input-append date"> <?php $this->widget('zii.widgets.jui.CJuiDatePicker', array( 'model' => $model, 'attribute' => 'due_date', 'htmlOptions' => array( 'size' => '10', 'maxlength' => '10', 'class' => 'form-control', 'value' => $model->due_date == "" ? "" : date("m/d/Y", $model->due_date) ), )); ?> </div> </div> </div> <div class="row buttons"> <?php echo CHtml::submitButton($model->isNewRecord ? 'Create' : 'Save', array('class' => 'btn btn-primary pull-right')); ?> </div> <?php $this->endWidget(); ?>
Our tasks application can now do everything we defined in our requirements. However, it is open to the world. Anyone who wants to edit our tasks could simply visit our website and change anything without our knowledge. Before finishing up, let's create a simple authentication system to protect our data.
The first part in protecting our application is making sure that only authorized people can visit our application. We can do this by adding a filter to our controller called accessControl
and defining access rules to access our content.
A filter is a piece of code that gets executed before (and/or after) a controller action runs, which means that the user will be required to be authenticated before accessing our content. To add the accessControl
filter, add the following to both TasksController
and ProjectsController
:
public function filters() { return array( 'accessControl', ); }
Next, create a new method called accessRules()
, which will define what users can access our application. For our application, we want to deny access to anyone who isn't authenticated. Have a look at the following code snippet:
public function accessRules() { return array( array('allow', 'users'=>array('@'), ), array('deny', // deny all users 'users'=>array('*'), ), ); }
In the preceding array, @
is a shorthand reference to an authenticated user. Now if we try to visit our web page, we'll be redirected to /site/login
, the default login
action in Yii.
Create a file called SiteController.php
in protected/controllers
, and then create login
and logout
actions as follows:
<?php class SiteController extends CController { public $layout = 'signin'; public function actionLogin() { $model = new LoginForm; if (isset($_POST['LoginForm'])) { $model->attributes = $_POST['LoginForm']; if ($model->login()) $this->redirect($this->createUrl('/projects')); } $this->render('login', array('model' => $model)); } public function actionLogout() { Yii::app()->user->logout(); $this->redirect($this->createUrl('/site/login')); } }
For this controller, we're going to create a new layout called login.php
in protected/views/layouts
. Copy the markup from protected/views/layouts/main.php
to our new layout, and replace the contents of the <body>
tag with the following:
<div class="row"> <div class="container"> <?php echo $content; ?> </div> </div>
To make our login page look more like a login page, add the following CSS to the layout either as an inline style or as a separate file in /css/signup.css
:
body { padding-top: 40px; padding-bottom: 40px; background-color: #eee; } .form-signin { max-width: 330px; padding: 15px; margin: 0 auto; } .form-signin .form-signin-heading, .form-signin .checkbox { margin-bottom: 10px; } .form-signin .checkbox { font-weight: normal; } .form-signin .form-control { position: relative; font-size: 16px; height: auto; padding: 10px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } .form-signin .form-control:focus { z-index: 2; } .form-signin input[type="text"] { margin-bottom: -1px; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; }
Create a new form in protected/views/site/login.php
that will hold our login model, as follows:
<?php $form=$this->beginWidget('CActiveForm', array( 'id'=>'login-form', 'enableClientValidation'=>true, 'htmlOptions' => array( 'class' => 'form-signin', 'role' => 'form' ), 'clientOptions'=>array( 'validateOnSubmit'=>true, ), )); ?> <?php if (!Yii::app()->user->isGuest): ?> <h2 class="form-signin-heading">You are already signed in! Please <?php echo CHtml::link('logout', $this->createUrl('/site/logout')); ?> first.</h2> <?php else: ?> <h2 class="form-signin-heading">Please sign in</h2> <?php echo $form->errorSummary($model); ?> <?php echo $form->textField($model,'username', array('class' => 'form-control', 'placeholder' => 'Username')); ?> <?php echo $form->passwordField($model,'password', array('class' => 'form-control', 'placeholder' => 'Password')); ?> <?php echo CHtml::tag('button', array('class' => 'btn btn-lg btn-primary btn-block'), 'Submit'); ?> <?php endif; ?> <?php $this->endWidget(); ?>
Before we create our login model, we need to create a way to identify our users. Fortunately, Yii has a built-in class to handle this called CUserIdentity
. By easily extending CUserIdentity
, we can create a key-value login pair that will ensure that only authenticated users can log in to our application.
Create a new file called UserIdentity.php
in /components
, and add the following:
<?php class UserIdentity extends CUserIdentity { public function authenticate() { $users=array( 'demo'=>'demo', 'admin'=>'admin', ); if(!isset($users[$this->username])) $this->errorCode=self::ERROR_USERNAME_INVALID; elseif($users[$this->username]!==$this->password) $this->errorCode=self::ERROR_PASSWORD_INVALID; else $this->errorCode=self::ERROR_NONE; return !$this->errorCode; } }
The authenticate()
method of UserIdentity
is what we'll use in our login model to ensure that we have valid credentials. In this class, we are simply checking whether the username
that will be sent to this class by our login model matches the key associated with it. If a user's password does not match the key in our $users
array, or if the user is not defined in our $users
array, we return an error code.
The last component we need to authenticate our users is to create a generic model to authenticate the user against. Begin by creating a new file called LoginForm.php
in protected/models
. This class will extend CFormModel
, a generic model in Yii for forms, as follows:
<?php class LoginForm extends CFormModel {
Since CFormModel
doesn't connect to a database, we defined attributes as public properties, as follows:
public $username; public $password; private $_identity;
Our model also needs validation rules to verify that we have a valid user. In addition to making sure username
and password
are provided, we're going to provide an additional validation rule called authenticate
, which will validate that we have a valid username and password. Have a look at the following lines of code:
public function rules() { return array( array('username, password', 'required'), array('password', 'authenticate'), ); }
Because our authenticate()
method is a custom validator, its method signature has two parameters, $attribute
and $params
, which have information about the attribute and parameters that may have been passed from the validator. This method will determine whether our credentials are valid. Have a look at the following code:
public function authenticate($attribute,$params) { if(!$this->hasErrors()) { $this->_identity=new UserIdentity($this->username,$this->password); if(!$this->_identity->authenticate()) $this->addError('password','Incorrect username or password.'); } }
Finally, we'll create the login()
method that our SiteController
calls. In addition to validating our credentials, it will do the heavy lifting of creating a session for the user. Have a look at the following code:
public function login() { if (!$this->validate()) return false; if ($this->_identity===null) { $this->_identity=new UserIdentity($this->username,$this->password); $this->_identity->authenticate(); } if ($this->_identity->errorCode===UserIdentity::ERROR_NONE) { $duration = 3600*24*30; Yii::app()->user->login($this->_identity,$duration); return true; } else return false; }
Now you can visit our site and log in with the credentials provided in our UserIdentity.php
file.
Before completing our project, there are a few things we need to take care of in our protected/config/main.php
file to enhance the security of our application and to make our application easier to use.
It would be nice to also add some pictures of the final application.
At the beginning of our project, we enabled the Gii module to assist us in creating models for our application. Since Gii has the ability to write new files to our project, we should remove the following section from our config
file:
'gii'=>array( 'class'=>'system.gii.GiiModule', 'password'=>false, 'ipFilters' => false ),
Presently, if we try to visit the root URL of our application, we are presented with an error. To avoid this, we can add a route in to the routes array of our URL Manager component. With this addition, whenever we visit the root URL of our application, we will be presented with the index
action of the project's controller. Have a look at the following code:
'components'=>array( [...] 'urlManager'=>array( [...] 'rules'=>array( [...] '/' => 'projects/index' ), ) )
In this chapter, we covered quite a lot of information. We created an automated way of creating and distributing our database, models to represent the tables in the database, and a few controllers to manage and interact with our data. We also created a simple key-value authentication system to protect our data. Many of the methods we used in this chapter, and the code we wrote, can be reused and expanded upon in later chapters. Before continuing, be sure to take a look at all the classes we referenced in the chapter, in the official Yii documentation, so that you can better understand them.