24 min read

In this article by Charles R. Portwood II, the author of Yii Project Blueprints, we will look at how to create a feature-complete content management system and blogging platform.

(For more resources related to this topic, see here.)

Describing the project

Our CMS can be broken down into several different components:

  • Users who will be responsible for viewing and managing the content
  • Content to be managed
  • Categories for our content to be placed into
  • Metadata to help us further define our content and users
  • Search engine optimizations

Users

The first component of our application is the users who will perform all the tasks in our application. For this application, we’re going to largely reuse the user database and authentication system. In this article, we’ll enhance this functionality by allowing social authentication. Our CMS will allow users to register new accounts from the data provided by Twitter; after they have registered, the CMS will allow them to sign-in to our application by signing in to Twitter.

To enable us to know if a user is a socially authenticated user, we have to make several changes to both our database and our authentication scheme. First, we’re going to need a way to indicate whether a user is a socially authenticated user. Rather than hardcoding a isAuthenticatedViaTwitter column in our database, we’ll create a new database table called user_metadata, which will be a simple table that contains the user’s ID, a unique key, and a value. This will allow us to store additional information about our users without having to explicitly change our user’s database table every time we want to make a change:

ID INTEGER PRIMARY KEY
user_id INTEGER
key STRING
value STRING
created INTEGER
updated INTEGER

We’ll also need to modify our UserIdentity class to allow socially authenticated users to sign in. To do this, we’ll be expanding upon this class to create a RemoteUserIdentity class that will work off the OAuth codes that Twitter (or any other third-party source that works with HybridAuth) provide to us rather than authenticating against a username and password.

Content

At the core of our CMS is our content that we’ll manage. For this project, we’ll manage simple blog posts that can have additional metadata associated with them. Each post will have a title, a body, an author, a category, a unique URI or slug, and an indication whether it has been published or not. Our database structure for this table will look as follows:

ID INTEGER PRIMARY KEY
title STRING
body TEXT
published INTEGER
author_id INTEGER
category_id INTEGER
slug STRING
created INTEGER
updated INTEGER

Each post will also have one or many metadata columns that will further describe the posts we’ll be creating. We can use this table (we’ll call it content_metadata) to have our system store information about each post automatically for us, or add information to our posts ourselves, thereby eliminating the need to constantly migrate our database every time we want to add a new attribute to our content:

ID INTEGER PRIMARY KEY
content_id INTEGER
key STRING
value STRING
created INTEGER
updated INTEGER

Categories

Each post will be associated with a category in our system. These categories will help us further refine our posts. As with our content, each category will have its own slug. Before either a post or a category is saved, we’ll need to verify that the slug is not already in use. Our table structure will look as follows:

ID INTEGER PRIMARY KEY
name STRING
description TEXT
slug STRING
created INTEGER
updated INTEGER

Search engine optimizations

The last core component of our application is optimization for search engines so that our content can be indexed quickly. SEO is important because it increases our discoverability and availability both on search engines and on other marketing materials. In our application, there are a couple of things we’ll perform to improve our SEO:

  • The first SEO enhancement we’ll add is a sitemap.xml file, which we can submit to popular search engines to index. Rather than crawl our content, search engines can very quickly index our sitemap.xml file, which means that our content will show up in search engines faster.
  • The second enhancement we’ll be adding is the slugs that we discussed earlier. Slugs allow us to indicate what a particular post is about directly from a URL. So rather than have a URL that looks like http://chapter6.example.com/content/post/id/5, we can have URL’s that look like: http://chapter6.example.com/my-awesome-article. These types of URLs allow search engines and our users to know what our content is about without even looking at the content itself, such as when a user is browsing through their bookmarks or browsing a search engine.

Initializing the project

To provide us with a common starting ground, a skeleton project has been included with the project resources for this article. Included with this skeleton project are the necessary migrations, data files, controllers, and views to get us started with developing. Also included in this skeleton project are the user authentication classes. Copy this skeleton project to your web server, configure it so that it responds to chapter6.example.com as outlined at the beginning of the article, and then perform the following steps to make sure everything is set up:

  1. Adjust the permissions on the assets and protected/runtime folders so that they are writable by your web server.
  2. In this article, we’ll once again use the latest version of MySQL (at the time of writing MySQL 5.6). Make sure that your MySQL server is set up and running on your server. Then, create a username, password, and database for our project to use, and update your protected/config/main.php file accordingly. For simplicity, you can use ch6_cms for each value.
  3. Install our Composer dependencies:
    Composer install
  4. Run the migrate command and install our mock data:
    php protected/yiic.php migrate up --interactive=0
    psql ch6_cms -f protected/data/postgres.sql
  5. Finally, add your SendGrid credentials to your protected/config/params.php file:
    'username' => '<username>',
    'password' => '<password>',
    'from' => '[email protected]'
    )

If everything is loaded correctly, you should see a 404 page similar to the following:

Yii Project Blueprints

Exploring the skeleton project

There are actually a lot of different things going on in the background to make this work even if this is just a 404 error. Before we start doing any development, let’s take a look at a few of the classes that have been provided in our skeleton project in the protected/components folder.

Extending models from a common class

The first class that has been provided to us is an ActiveRecord extension called CMSActiveRecord that all of our models will stem from. This class allows us to reduce the amount of code that we have to write in each class. For now, we’ll simply add CTimestampBehavior and the afterFind() method to store the old attributes for the time the need arises to compare the changed attributes with the new attributes:

class CMSActiveRecordCMSActiveRecord extends CActiveRecord
{
public $_oldAttributes = array();
public function behaviors()
{
return array(
'CTimestampBehavior' => array(
'class' => 'zii.behaviors.CTimestampBehavior',
'createAttribute' => 'created',
'updateAttribute' => 'updated',
'setUpdateOnCreate' => true
)
);
}
public function afterFind()
{
if ($this !== NULL)
$this->_oldAttributes = $this->attributes;
return parent::afterFind();
}
}

Creating a custom validator for slugs

Since both Content and Category classes have slugs, we’ll need to add a custom validator to each class that will enable us to ensure that the slug is not already in use by either a post or a category. To do this, we have another class called CMSSlugActiveRecord that extends CMSActiveRecord with a validateSlug() method that we’ll implement as follows:

class CMSSLugActiveRecord extends CMSActiveRecord
{
public function validateSlug($attributes, $params)
{
// Fetch any records that have that slug
$content = Content::model()->findByAttributes(array('slug' =>
$this->slug));
$category = Category::model()->findByAttributes(array('slug' =>
$this->slug));
$class = strtolower(get_class($this));
if ($content == NULL && $category == NULL)
return true;
else if (($content == NULL && $category != NULL) || ($content !=
NULL && $category == NULL))
{
$this->addError('slug', 'That slug is already in use');
return false;
}
else
{
if ($this->id == $$class->id)
return true;
}
$this->addError('slug', 'That slug is already in use');
return false;
}
}

This implementation simply checks the database for any item that has that slug. If nothing is found, or if the current item is the item that is being modified, then the validator will return true. Otherwise, it will add an error to the slug attribute and return false. Both our Content model and Category model will extend from this class.

View management with themes

One of the largest challenges of working with larger applications is changing their appearance without locking functionality into our views. One way to further separate our business logic from our presentation logic is to use themes. Using themes in Yii, we can dynamically change the presentation layer of our application simply utilizing the Yii::app()->setTheme(‘themename) method. Once this method is called, Yii will look for view files in themes/themename/views rather than protected/views. Throughout the rest of the article, we’ll be adding views to a custom theme called main, which is located in the themes folder. To set this theme globally, we’ll be creating a custom class called CMSController, which all of our controllers will extend from. For now, our theme name will be hardcoded within our application. This value could easily be retrieved from a database though, allowing us to dynamically change themes from a cached or database value rather than changing it in our controller. Have a look at the following lines of code:

class CMSController extends CController
{
public function beforeAction($action)
{
Yii::app()->setTheme('main');
return parent::beforeAction($action);
}
}

Truly dynamic routing

In our previous applications, we had long, boring URL’s that had lots of IDs and parameters in them. These URLs provided a terrible user experience and prevented search engines and users from knowing what the content was about at a glance, which in turn would hurt our SEO rankings on many search engines. To get around this, we’re going to heavily modify our UrlManager class to allow truly dynamic routing, which means that, every time we create or update a post or a category, our URL rules will be updated.

Telling Yii to use our custom UrlManager

Before we can start working on our controllers, we need to create a custom UrlManager to handle routing of our content so that we can access our content by its slug. The steps are as follows:

  1. The first change we need to make to allow for this routing is to update the components section of our protected/config/main.php file. This will tell Yii what class to use for the UrlManager component:
    'urlManager' => array(
    'class' => 'application.components.
    CMSUrlManager',
    'urlFormat' => 'path',
    'showScriptName' => false
    )
  2. Next, within our protected/components folder, we need to create CMSUrlManager.php:
    class CMSUrlManager extends CUrlManager {}
  3. CUrlManager works by populating a rules array. When Yii is bootstrapped, it will trigger the processRules() method to determine which route should be executed. We can overload this method to inject our own rules, which will ensure that the action that we want to be executed is executed.
  4. To get started, let’s first define a set of default routes that we want loaded. The routes defined in the following code snippet will allow for pagination on our search and home page, enable a static path for our sitemap.xml file, and provide a route for HybridAuth to use for social authentication:
    public $defaultRules = array(
    '/sitemap.xml' => '/content/sitemap',
    '/search/<page:d+>' => '/content/search',
    '/search' => '/content/search',
    '/blog/<page:d+>' => '/content/index',
    '/blog' => '/content/index',
    '/' => '/content/index',
    '/hybrid/<provider:w+>' => '/hybrid/index',
    );
  5. Then, we’ll implement our processRules() method:
    protected function processRules() {}
  6. CUrlManager already has a public property that we can interface to modify the rules, so we’ll inject our own rules into this. The rules property is the same property that can be accessed from within our config file. Since processRules() gets called on every page load, we’ll also utilize caching so that our rules don’t have to be generated every time. We’ll start by trying to load any of our pregenerated rules from our cache, depending upon whether we are in debug mode or not:
    $this->rules = !YII_DEBUG ? Yii::app()->cache->get('Routes') : array();

    If the rules we get back are already set up, we’ll simple return them; otherwise, we’ll generate the rules, put them into our cache, and then append our basic URL rules:

    if ($this->rules == false || empty($this->rules))
    {
    $this->rules = array();
    $this->rules = $this->generateClientRules();
    $this->rules = CMap::mergearray($this->addRssRules(), $this-
    >rules);
    Yii::app()->cache->set('Routes', $this->rules);
    }
    $this->rules['<controller:w+>/<action:w+>/<id:w+>'] =
    '/';
    $this->rules['<controller:w+>/<action:w+>'] =
    '/';
    return parent::processRules();
  7. For abstraction purposes, within our processRules() method, we’ve utilized two methods we’ll need to create: generateClientRules, which will generate the rules for content and categories, and addRSSRules, which will generate the RSS routes for each category.

    The first method, generateClientRules(), simply loads our default rules that we defined earlier with the rules generated from our content and categories, which are populated by the generateRules() method:

    private function generateClientRules()
    {
    $rules = CMap::mergeArray($this->defaultRules, $this->rules);
    return CMap::mergeArray($this->generateRules(), $rules);
    }
    private function generateRules()
    {
    return CMap::mergeArray($this->generateContentRules(), $this-
    >generateCategoryRules());
    }
    
  8. The generateRules() method, that we just defined, actually calls the methods that build our routes. Each route is a key-value pair that will take the following form:
    array(
    '<slug>' => '<controller>/<action>/id/<id>'
    )

    Content rules will consist of an entry that is published. Have a look at the following code:

    private function generateContentRules()
    {
    $rules = array();
    $criteria = new CDbCriteria;
    $criteria->addCondition('published = 1');
    $content = Content::model()->findAll($criteria);
    foreach ($content as $el)
    {
    if ($el->slug == NULL)
    continue;
    $pageRule = $el->slug.'/<page:d+>';
    $rule = $el->slug;
    if ($el->slug == '/')
    $pageRule = $rule = '';
    $pageRule = $el->slug . '/<page:d+>';
    $rule = $el->slug;
    $rules[$pageRule] = "content/view/id/{$el->id}";
    $rules[$rule] = "content/view/id/{$el->id}";
    }
    return $rules;
    }
  9. Our category rules will consist of all categories in our database. Have a look at the following code:
    private function generateCategoryRules()
    {
    $rules = array();
    $categories = Category::model()->findAll();
    foreach ($categories as $el)
    {
    if ($el->slug == NULL)
    continue;
    $pageRule = $el->slug.'/<page:d+>';
    $rule = $el->slug;
    if ($el->slug == '/')
    $pageRule = $rule = '';
    $pageRule = $el->slug . '/<page:d+>';
    $rule = $el->slug;
    $rules[$pageRule] = "category/index/id/{$el->id}";
    $rules[$rule] = "category/index/id/{$el->id}";
    }
    return $rules;
    }
  10. Finally, we’ll add our RSS rules that will allow RSS readers to read all content for the entire site or for a particular category, as follows:
    private function addRSSRules()
    {
    $categories = Category::model()->findAll();
    foreach ($categories as $category)
    $routes[$category->slug.'.rss'] = "category/rss/id/
    {$category->id}";
    $routes['blog.rss'] = '/category/rss';
    return $routes;
    }

Displaying and managing content

Now that Yii knows how to route our content, we can begin work on displaying and managing it. Begin by creating a new controller called ContentController in protected/controllers that extends CMSController. Have a look at the following line of code:

class ContentController extends CMSController {}

To start with, we’ll define our accessRules() method and the default layout that we’re going to use. Here’s how:

public $layout = 'default';
public function filters()
{
return array(
'accessControl',
);
}
public function accessRules()
{
return array(
array('allow',
'actions' => array('index', 'view', 'search'),
'users' => array('*')
),
array('allow',
'actions' => array('admin', 'save', 'delete'),
'users'=>array('@'),
'expression' => 'Yii::app()->user->role==2'
),
array('deny', // deny all users
'users'=>array('*'),
),
);
}

Rendering the sitemap

The first method we’ll be implementing is our sitemap action. In our ContentController, create a new method called actionSitemap():

public function actionSitemap() {}

The steps to be performed are as follows:

  1. Since sitemaps come in XML formatting, we’ll start by disabling WebLogRoute defined in our protected/config/main.php file. This will ensure that our XML validates when search engines attempt to index it:
    Yii::app()->log->routes[0]->enabled = false;
  2. We’ll then send the appropriate XML headers, disable the rendering of the layout, and flush any content that may have been queued to be sent to the browser:
    ob_end_clean();
    header('Content-type: text/xml; charset=utf-8');
    $this->layout = false;
  3. Then, we’ll load all the published entries and categories and send them to our sitemap view:
    $content = Content::model()->findAllByAttributes(array('published'
    => 1));
    $categories = Category::model()->findAll();
    $this->renderPartial('sitemap', array(
    'content' => $content,
    'categories' => $categories,
    'url' => 'http://'.Yii::app()->request->serverName .
    Yii::app()->baseUrl
    ))
  4. Finally, we have two options to render this view. We can either make it a part of our theme in themes/main/views/content/sitemap.php, or we can place it in protected/views/content/sitemap.php. Since a sitemap’s structure is unlikely to change, let’s put it in the protected/views folder:
    <?php echo '<?xml version="1.0" encoding="UTF-8"?>'; ?>
    <urlset >
    <?php foreach ($content as $v): ?>
    <url>
    <loc><?php echo $url .'/'. htmlspecialchars(str_
    replace('/', '', $v['slug']), ENT_QUOTES, "utf-8"); ?></loc>
    <lastmod><?php echo date('c',
    strtotime($v['updated']));?></lastmod>
    <changefreq>weekly</changefreq>
    <priority>1</priority>
    </url>
    <?php endforeach; ?>
    <?php foreach ($categories as $v): ?>
    <url>
    <loc><?php echo $url .'/'. htmlspecialchars(str_
    replace('/', '', $v['slug']), ENT_QUOTES, "utf-8"); ?></loc>
    <lastmod><?php echo date('c',
    strtotime($v['updated']));?></lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.7</priority>
    </url>
    <?php endforeach; ?>
    </urlset>

You can now load http://chapter6.example.com/sitemap.xml in your browser to see the sitemap. Before you make your site live, be sure to submit this file to search engines for them to index.

Displaying a list view of content

Next, we’ll implement the actions necessary to display all of our content and a particular post. We’ll start by providing a paginated view of our posts. Since CListView and the Content model’s search() method already provide this functionality, we can utilize those classes to generate and display this data:

  1. To begin with, open protected/models/Content.php and modify the return value of the search() method as follows. This will ensure that Yii’s pagination uses the correct variable in our CListView, and tells Yii how many results to load per page.
    return new CActiveDataProvider($this, array(
    'criteria' =>$criteria,
    'pagination' => array(
    'pageSize' => 5,
    'pageVar' =>'page'
    )
    ));
  2. Next, implement the actionIndex() method with the $page parameter. We’ve already told our UrlManager how to handle this, which means that we’ll get pretty URI’s for pagination (for example, /blog, /blog/2, /blog/3, and so on):
    public function actionIndex($page=1)
    {
    // Model Search without $_GET params
    $model = new Content('search');
    $model->unsetAttributes();
    $model->published = 1;
    $this->render('//content/all', array(
    'dataprovider' => $model->search()
    ));
    }
  3. Then we’ll create a view in themes/main/views/content/all.php, that will display the data within our dataProvider:
    <?php $this->widget('zii.widgets.CListView', array(
    'dataProvider'=>$dataprovider,
    'itemView'=>'//content/list',
    'summaryText' => '',
    'pager' => array(
    'htmlOptions' => array(
    'class' => 'pager'
    ),
    'header' => '',
    'firstPageCssClass'=>'hide',
    'lastPageCssClass'=>'hide',
    'maxButtonCount' => 0
    )
    ));
  4. Finally, copy themes/main/views/content/all.php from the project resources folder so that our views can render.

Since our database has already been populated with some sample data, you can start playing around with the results right away, as shown in the following screenshot:

Yii Project Blueprints

Displaying content by ID

Since our routing rules are already set up, displaying our content is extremely simple. All that we have to do is search for a published model with the ID passed to the view action and render it:

public function actionView($id=NULL)
{
// Retrieve the data
$content = Content::model()->findByPk($id);
// beforeViewAction should catch this
if ($content == NULL || !$content->published)
throw new CHttpException(404, 'The article you specified does
not exist.');
$this->render('view', array(
'id' => $id,
'post' => $content
));
}

After copying themes/main/views/content/view.php from the project resources folder into your project, you’ll be able to click into a particular post from the home page. In its actions present form, this action has introduced an interesting side effect that could negatively impact our SEO rankings on search engines—the same entry can now be accessed from two URI’s. For example, http://chapter6.example.com/content/view/id/1 and http://chapter6.example.com/quis-condimentum-tortor now bring up the same post. Fortunately, correcting this bug is fairly easy. Since the goal of our slugs is to provide more descriptive URI’s, we’ll simply block access to the view if a user tries to access it from the non-slugged URI.

We’ll do this by creating a new method called beforeViewAction() that takes the entry ID as a parameter and gets called right after the actionView() method is called. This private method will simply check the URI from CHttpRequest to determine how actionView was accessed and return a 404 if it’s not through our beautiful slugs:

private function beforeViewAction($id=NULL)
{
// If we do not have an ID, consider it to be null, and throw a 404
error
if ($id == NULL)
throw new CHttpException(404,'The specified post cannot be
found.');
// Retrieve the HTTP Request
$r = new CHttpRequest();
// Retrieve what the actual URI
$requestUri = str_replace($r->baseUrl, '', $r->requestUri);
// Retrieve the route
$route = '/' . $this->getRoute() . '/' . $id;
$requestUri = preg_replace('/?(.*)/','',$requestUri);
// If the route and the uri are the same, then a direct access
attempt was made, and we need to block access to the controller
if ($requestUri == $route)
throw new CHttpException(404, 'The requested post cannot be
found.');
return str_replace($r->baseUrl, '', $r->requestUri);
}

Then right after our actionView starts, we can simultaneously set the correct return URL and block access to the content if it wasn’t accessed through the slug as follows:

Yii::app()->user->setReturnUrl($this->beforeViewAction($id));

Adding comments to our CMS with Disqus

Presently, our content is only informative in nature—we have no way for our users to communicate with us what they thought about our entry. To encourage engagement, we can add a commenting system to our CMS to further engage with our readers. Rather than writing our own commenting system, we can leverage comment through Disqus, a free, third-party commenting system. Even through Disqus, comments are implemented in JavaScript and we can create a custom widget wrapper for it to display comments on our site. The steps are as follows:

  1. To begin with, log in to the Disqus account you created at the beginning of this article as outlined in the prerequisites section. Then, navigate to http://disqus.com/admin/create/ and fill out the form fields as prompted and as shown in the following screenshot:
    Yii Project Blueprints
  2. Then, add a disqus section to your protected/config/params.php file with your site shortname:
    'disqus' => array(
    'shortname' => 'ch6disqusexample',
    )
  3. Next, create a new widget in protected/components called DisqusWidget.php. This widget will be loaded within our view and will be populated by our Content model:
    class DisqusWidget extends CWidget {}
  4. Begin by specifying the public properties that our view will be able to inject into as follows:
    public $shortname = NULL;
    public $identifier = NULL;
    public $url = NULL;
    public $title = NULL;
  5. Then, overload the init() method to load the Disqus JavaScript callback and to populate the JavaScript variables with those populated to the widget as follows:
    public function init()
    public function init()
    {
    parent::init();
    if ($this->shortname == NULL)
    throw new CHttpException(500, 'Disqus shortname is
    required');
    echo "<div id='disqus_thread'></div>";
    Yii::app()->clientScript->registerScript('disqus', "
    var disqus_shortname = '{$this->shortname}';
    var disqus_identifier = '{$this->identifier}';
    var disqus_url = '{$this->url}';
    var disqus_title = '{$this->title}';
    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
    var dsq = document.createElement('script'); dsq.type =
    'text/javascript'; dsq.async = true;
    dsq.src = '//' + disqus_shortname + '.disqus.com/
    embed.js';
    (document.getElementsByTagName('head')[0] || document.
    getElementsByTagName('body')[0]).appendChild(dsq);
    })();
    ");
    }
  6. Finally, within our themes/main/views/content/view.php file, load the widget as follows:
    <?php $this->widget('DisqusWidget', array(
    'shortname' => Yii::app()->params['includes']['disqus']
    ['shortname'],
    'url' => $this->createAbsoluteUrl('/'.$post->slug),
    'title' => $post->title,
    'identifier' => $post->id
    )); ?>

Now, when you load any given post, Disqus comments will also be loaded with that post. Go ahead and give it a try!

Yii Project Blueprints

Searching for content

Next, we’ll implement a search method so that our users can search for posts. To do this, we’ll implement an instance of CActiveDataProvider and pass that data to our themes/main/views/content/all.php view to be rendered and paginated:

public function actionSearch()
{
$param = Yii::app()->request->getParam('q');
$criteria = new CDbCriteria;
$criteria->addSearchCondition('title',$param,'OR');
$criteria->addSearchCondition('body',$param,'OR');
$dataprovider = new CActiveDataProvider('Content', array(
'criteria'=>$criteria,
'pagination' => array(
'pageSize' => 5,
'pageVar'=>'page'
)
));
$this->render('//content/all', array(
'dataprovider' => $dataprovider
));
}

Since our view file already exists, we can now search for content in our CMS.

Managing content

Next, we’ll implement a basic set of management tools that will allow us to create, update, and delete entries:

  1. We’ll start by defining our loadModel() method and the actionDelete() method:
    private function loadModel($id=NULL)
    {
    if ($id == NULL)
    throw new CHttpException(404, 'No category with that ID
    exists');
    $model = Content::model()->findByPk($id);
    if ($model == NULL)
    throw new CHttpException(404, 'No category with that ID
    exists');
    return $model;
    }
    public function actionDelete($id)
    {
    $this->loadModel($id)->delete();
    $this->redirect($this->createUrl('content/admin'));
    }
  2. Next, we can implement our admin view, which will allow us to view all the content in our system and to create new entries. Be sure to copy the themes/main/views/content/admin.php file from the project resources folder into your project before using this view:
    public function actionAdmin()
    {
    $model = new Content('search');
    $model->unsetAttributes();
    if (isset($_GET['Content']))
    $model->attributes = $_GET;
    $this->render('admin', array(
    'model' => $model
    ));
    }
  3. Finally, we’ll implement a save view to create and update entries. Saving content will simply pass it through our content model’s validation rules. The only override we’ll be adding is ensuring that the author is assigned to the user editing the entry. Before using this view, be sure to copy the themes/main/views/content/save.php file from the project resources folder into your project:
    public function actionSave($id=NULL)
    {
    if ($id == NULL)
    $model = new Content;
    else
    $model = $this->loadModel($id);
    if (isset($_POST['Content']))
    {
    $model->attributes = $_POST['Content'];
    $model->author_id = Yii::app()->user->id;
    if ($model->save())
    {
    Yii::app()->user->setFlash('info', 'The articles was
    saved');
    $this->redirect($this->createUrl('content/admin'));
    }
    }
    $this->render('save', array(
    'model' => $model
    ));
    }

At this point, you can now log in to the system using the credentials provided in the following table and start managing entries:

Username

Password

[email protected]

test

[email protected]

test

Summary

In this article, we dug deeper into Yii framework by manipulating our CUrlManager class to generate completely dynamic and clean URIs. We also covered the use of Yii’s built-in theming to dynamically change the frontend appearance of our site by simply changing a configuration value.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here