Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 12, 2012 09:39 pm GMT

Zend Framework from Scratch Models and Integrating Doctrine ORM

Ready to take your PHP skills to the next level? In this new “From Scratch” series, we’ll focus exclusively on Zend Framework, a full-stack PHP framework created by Zend Technologies. This second tutorial on our series is entitled “Models and Integrating Doctrine ORM”.


Review

Welcome back to our Zend Framework from Scratch series! In our last tutorial, we learned some basic things about Zend Framework, such as:

  • Where to download the latest Zend Framework files
  • Where and how to set it up locally
  • Creating your first Zend Framework project and setting up a VirtualHost on your web server
  • How exactly Zend Framework implements the MVC pattern and its default application routing
  • Passing data from a controller to its view
  • Creating a site-wide layout for your Zend Framework application
  • Creating new controllers and actions

If you haven’t yet, you should give the previous tutorial a read. It will really make it easier to for you to understand some Zend Framework basics and help you understand some things we discuss in this tutorial.

In this second part of the series, we’ll be talking about a crucial part of any web application — the MODELS. We’ll also be taking a look at how to integrate the very popular Doctrine ORM with our Zend Framework project, and find out why it’s much better to use than Zend Framework’s native Zend_Db. So, without further ado, let’s begin!


What exactly are “Models”?

When I started trying to grasp the concept of MVC, I read quite a number of analogies, which attempted to explain exactly what each of these components represent. One of the best analogies I’ve read so far was from this article, Another way to think about MVC. It goes something like this:

So, let’s imagine a bank.

The safe is the Database this is where all the most important goodies are stored, and are nicely protected from the outside world.

Then we have the bankers or in programmatic terms the Models. The bankers are the only ones who have access to the safe (the DB). They are generally fat, old and lazy, which follows quite nicely with one of the rules of MVC: *fat Models, skinny controllers*. We’ll see why and how this analogy applies a little later.

Now we’ve got our average bank workers, the gophers, the runners, the Controllers. Controllers or gophers do all the running around, that’s why they have to be fit and skinny. They take the loot or information from the bankers (the Models) and bring it to the bank customers the Views.

The bankers (Models) have been at the job for a while, therefore they make all the important decisions. Which brings us to another rule: *keep as much business logic in the Model as possible*. The controllers, our average workers, should not be making such decisions, they ask the banker for details, get the info, and pass it on to the customer (the View). Hence, we continue to follow the rule of *fat Models, skinny controllers*. The gophers do not make important decisions, but they cannot be plain dumb (thus a little business logic in the controller is OK). However, as soon as the gopher begins to think too much the banker gets upset and your bank (or you app) goes out of business. So again, always remember to offload as much business logic (or decision making) to the model.

Now, the bankers sure as hell aren’t going to talk to the customers (the View) directly, they are way too important in their cushy chairs for that. Thus another rule is followed: *Models should not talk to Views*. This communication between the banker and the customer (the Model and the View) is always handled by the gopher (the Controller). (Yes, there are some exception to this rule for super VIP customers, but let’s stick to basics for the time being).

It also happens that a single worker (Controller) has to get information from more than one banker, and that’s perfectly acceptable. However, if the bankers are related (otherwise how else would they land such nice jobs?) the bankers (Models) will communicate with each other first, and then pass cumulative information to their gopher, who will happily deliver it to the customer (View). So here’s another rule: *Related Models provide information to the controller via their association (relation)*.

So what about our customer (the View)? Well, banks do make mistakes and the customer should be smart enough to balance their own account and make some decisions. In MVC terms we get another simple rule: *it’s quite alright for the views to contain some logic, which deals with the view or presentation*. Following our analogy, the customer will make sure not forget to wear pants while they go to the bank, but they are not going to tell the bankers how to process the transactions.

In a nutshell:

  • Models are representatives of the Database, and should be where all the business logic of an application resides
  • Controllers communicate with Models and ask them to retrieve information they need
  • This information is then passed by a Controller to the View and is rendered
  • It’s very rare that a Model directly interacts with a View, but sometimes it may happen when necessary
  • Models can talk with other Models and aren’t self-contained. They have relationships that intertwine with each other
  • These relationships make it easier and quicker for a Controller to get information, since it doesn’t have to interact with different Models – the Models can do that themselves

We can see how important Models are in any application, since it’s repsonsible for any dynamic actions that happens in an application. Now that we have a pretty clear understanding of the responsibilities of the Model, as well as the View and Controller, let’s dive into implementing the Models in our application.


Step 1 - Setting up your Zend Application to Connect to a Database

The first thing we’ll need to do is to make our Zend application connect to a database. Luckily, the zf command can take care of that. Open your Command Prompt (or Terminal), cd into your thenextsocial folder, and type in the following:

zf configure db-adapter "adapter=PDO_MYSQL&dbname=thenextsocial&host=localhost&username=[your local database username]&password=[your local database password]" -s development

If correct, you should get an output similar to:

A db configuration for the development section has been written to the application config file.

Additionally, you should see two new lines inside your application.ini file:

resources.db.adapter = "PDO_MYSQL"<br />resources.db.params.dbname = "thenextsocial"<br />resources.db.params.host = "localhost"<br />resources.db.params.username = "[your local database username]"<br />resources.db.params.password = "[your local database password]"<br /></p></code><h3>The application.ini explained</h3><p>The <code>application.ini</code> is a configuration file which should contain all of the configuration we have for an application. This includes, for example, what kind of database we're using, what the database name is, the username and password we'll be using with the database, even custom PHP settings like error display and include paths.</p><div class="tutorial_image"><img src="https://d2o0t5hpnwv4c1.cloudfront.net/1122_zend2/images/application_ini.png" alt="The application.ini file" title="The application.ini file" /><br /><small>The <code>application.ini</code> file</small></div><p>I'm sure you've noticed that the <code>application.ini</code> file has sections enclosed in [square brackets]. One of the great things about the <code>application.ini</code> is that you can define different settings depending on what environment your code is in. For example, the database parameters we created earlier falls under the <code>[development : production]</code> section, which means that the set of settings under this section will be used when the application is being run on the <code>development</code> environment.</p><p>To add to that, you can &ldquo;inherit&rdquo; settings from another section. For example, the <code>[development : production]</code> section is the configuration for the <code>development</code> environmnent, but inherits all the settings from the <code>production</code> environment as well. This means that any setting which you haven't explicitly overwritten in <code>development</code> will use the setting from <code>production</code>. This allows you to configure settings that are the same in all environments in one place, and just override the ones that you need. Pretty nifty huh?</p><p>To configure our project to use the <code>development</code> configuration settings, open or create an <strong>.htaccess</strong> file inside the <strong>public_html</strong> folder, and make sure that it looks like this:</p>1SetEnv APPLICATION_ENV development<br />RewriteEngine On<br />RewriteCond %{REQUEST_FILENAME} -s [OR]<br />RewriteCond %{REQUEST_FILENAME} -l [OR]<br />RewriteCond %{REQUEST_FILENAME} -d<br />RewriteRule ^.*$ - [NC,L]<br />RewriteRule ^.*$ index.php [NC,L]<br />

We can clearly see that the SetEnv APPLICATION_ENV directive sets our application’s environment. If and when we move the application to another environment, this should be the only thing we need to change. This ensures that everything our application relies on to work is defined in the application.ini, which makes sure that our application isn’t relying on any external setting. This helps eliminate the “it works on my development machine, how come it doesn’t work on the production server?” problem.


Step 2 - Create the Database and Some Tables

Before we create your our first Model for the application, we’ll need a Database that the Model will represent first. Let’s start with something simple — a User table, where we’ll save all the registered users for TheNextSocial.

Login to your MySQL database and create a database called thenextsocial. Once created, execute the following query to create a User table, and an accompanying User Settings table:

CREATE TABLE `thenextsocial`.`user` (  `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,  `email` VARCHAR(100) NOT NULL,  `password` TEXT NOT NULL,  `salt` TEXT NOT NULL,  `date_created` DATETIME NOT NULL,  PRIMARY KEY (`id`),  UNIQUE INDEX `Index_email`(`email`))ENGINE = InnoDBCHARACTER SET utf8 COLLATE utf8_general_ci;CREATE TABLE `thenextsocial`.`user_settings` (  `id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,  `user_id` INTEGER UNSIGNED NOT NULL,  `name` VARCHAR(100) NOT NULL,  `value` TEXT NOT NULL,  PRIMARY KEY (`id`),  CONSTRAINT `FK_user_settings_user_id` FOREIGN KEY `FK_user_settings_user_id` (`user_id`)    REFERENCES `user` (`id`)    ON DELETE CASCADE    ON UPDATE CASCADE)ENGINE = InnoDBCHARACTER SET utf8 COLLATE utf8_general_ci;

These SQL queries should create two tables. A user table with the following columns:

  • id – a unique ID for each user
  • email – the email address of the user, also unique
  • password – the user’s password, which we’ll hash
  • salt – a random salt, which we’ll use to hash the user’s password
  • date_created – the date and time the user record was created

And a user_settings table, where we’ll store any user-related settings, with the columns:

  • id – a unique ID for each setting
  • user_id – the user_id which is a foreign key to user.id
  • name – a string of text that represents the setting
  • value – the value of the setting

It’s worth taking note that the User and User Settings table share a One-to-Many relationship, which means a single User record can be related to multiple User Settings records. This will make it easier to store any kind of information related to a user, for example, their name or profile photo.

Now that we have a few tables to play around with, let’s learn how to create our first Model: the User Model.


Step 3 - Creating your First Model

The DAO Design Pattern

As with a lot of applications, the usual way to use make models in Zend Framework is to make use of a popular design pattern called the “DAO” pattern. In this pattern we have the following components:

  • Table Data Gateway (DataSource) which connects our application to the data source, the MySQL table
  • Data Mapper (DataAccessObject) which maps the data retrieved from the database to the
  • Data Object (Data) which represents a row from our database, after the DataMapper maps the data to it

Let’s begin by creating a Table Data Gateway for the User table using the zf CLI tool:

zf create db-table User user<br />Creating a DbTable at thenextsocial/application/models/DbTable/User.php<br />Updating project profile 'thenextsocial/.zfproject.xml'

The zf create db-table takes in two parameters:

  • ClassName – the name of the class
  • database_table – the name of the table

Open the User.php file found in the application/models/DbTable folder and it should look like this:

<?phpclass Application_Model_DbTable_User extends Zend_Db_Table_Abstract{    protected $_name = 'user';}

Now let’s create a Data Mapper class. Again, using the zf CLI tool:

zf create model UserMapper<br />Creating a model at thenextsocial/application/models/UserMapper.php<br />Updating project profile 'thenextsocial/.zfproject.xml'

The UserMapper.php file will be empty right now but we’ll put in some code later. For now, we need to create the Data Object, which is the User model:

zf create model User<br />Creating a model at thenextsocial/application/models/User.php<br />Updating project profile 'thenextsocial/.zfproject.xml'</p></code><p>Now that we have all three components of the DAO pattern, we create the code for the files. Open the <code>UserMapper.php</code> file and put in the following code:</p>1<?phpclass Application_Model_UserMapper{protected $_db_table;public function __construct(){//Instantiate the Table Data Gateway for the User table$this->_db_table = new Application_Model_DbTable_User();}public function save(Application_Model_User $user_object){//Create an associative array//of the data you want to update$data = array('email' => $user_object->email,'password' => $user_object->password,);//Check if the user object has an ID//if no, it means the user is a new user//if yes, then it means you're updating an old userif( is_null($user_object->id) ) {$data['salt'] = $user_object->salt;$data['date_created'] = date('Y-m-d H:i:s');$this->_db_table->insert($data);} else {$this->_db_table->update($data, array('id = ?' => $user_object->id));}}public function getUserById($id){//use the Table Gateway to find the row that//the id represents$result = $this->_db_table->find($id);//if not found, throw an exsceptionif( count($result) == 0 ) {throw new Exception('User not found');}//if found, get the result, and map it to the//corresponding Data Object$row = $result->current();$user_object = new Application_Model_User($row);//return the user objectreturn $user_object;}}

Here we have three methods:

  • __construct() – constructor for the class. Once instantiated, it creates an instance of the Table Data Gateway and stores it
  • save(Application_Model_User $user_object) – takes in a Application_Model_User and saves the data from the object to the database
  • getUserById($id) – takes in an integer $id which represents a single row from the database table, retrieves it, then returns a Application_Model_User with the data mapped

Open up User.php and put the following code in:

<?phpclass Application_Model_User{//declare the user's attributesprivate $id;private $email;private $password;private $salt;private $date_created;//upon construction, map the values//from the $user_row if availablepublic function __construct($user_row = null){if( !is_null($user_row) && $user_row instanceof Zend_Db_Table_Row ) {$this->id = $user_row->id;$this->email = $user_row->email;$this->password = $user_row->password;$this->salt = $user_row->salt;$this->date_created = $user_row->date_created;}}//magic function __set to set the//attributes of the User modelpublic function __set($name, $value){switch($name) {case 'id'://if the id isn't null, you shouldn't update it!if( !is_null($this->id) ) {throw new Exception('Cannot update User\'s id!');}break;case 'date_created'://same goes for date_createdif( !is_null($this->date_created) ) {throw new Exception('Cannot update User\'s date_created');}break;case 'password'://if you're updating the password, hash it first with the salt$value = sha1($value.$this->salt);break;}//set the attribute with the value$this->$name = $value;}public function __get($name){return $this->$name;}}
  • __construct($user_row) – takes in an optional Zend_Db_Table_Row object, which represents one row from the database, and maps the data to itself
  • __set($name, $value) – a magic function that takes care of setting all of the attributes for the model.
  • __get($name) – a magic function that takes care of getting an attribute of the model.

Let’s try it out! If you followed the previous tutorial, you should have an IndexController.php file. Open it and put in this code that creates a new user:

public function indexAction(){// action body$this->view->current_date_and_time = date('M d, Y - H:i:s');$user = new Application_Model_User();$user->email = '[email protected]';$user->salt = sha1(time());$user->password = 'test';$user->date_created = date('Y-m-d H:i:s');$user_mapper = new Application_Model_UserMapper();$user_mapper->save($user);}

Now go to https://thenextsocial.local/. Once it loads, check the thenextsocial.user table on MySQL and if everything worked, you should have a new User record!

A new User record!

A new User record!

Now, let’s try updating this record. Go back to IndexController.php and update the code to match the following:

public function indexAction(){// action body$this->view->current_date_and_time = date('M d, Y - H:i:s');$user_mapper = new Application_Model_UserMapper();$user = $user_mapper->getUserById(1);$user->email = '[email protected]';$user_mapper->save($user);}

Check the MySQL table again, and you should see that the email for the record has been updated!

Updated User record

Updated User record

Congratulations! You’ve successfully created your first ever Zend Framework Model!


The Doctrine ORM

Introduction

From the Doctrine ORM website, https://doctrine-project.org/projects/orm:

Object relational mapper (ORM) for PHP that sits on top of a powerful database abstraction layer (DBAL). One of its key features is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL), inspired by Hibernates HQL. This provides developers with a powerful alternative to SQL that maintains flexibility without requiring unnecessary code duplication.

Basically, the Doctrine ORM library abstracts most, if not all of the Model implementation for an application. Some of the incredible advantages I discovered while using Doctrine with Zend Framework are:

  • Very easy to use, guaranteed to cut your development time in half
  • Works just as well with different kinds of DB’s with very little tweaking needed. For example, Doctrine made it very easy for me to port an application I worked on before from using MySQL to MSSQL
  • A scaffolding tool, called Doctrine_Cli that creates models from the database very quickly

To get started, you should download the Doctrine ORM library first from their site. I’ll be using the 1.2.4 version. Go to https://www.doctrine-project.org/projects/orm/1.2/download/1.2.4 and click on the Download 1.2.4 Package link.

Downloading the Doctrine ORM

Downloading the Doctrine ORM

Once downloaded, open it and extract the contents. Inside, you should see Doctrine-1.2.4 folder and a package.xml file. Go inside the folder and you should see a Doctrine folder, a Doctrine.php file, and a LICENSE file.

Doctrine download contents

Doctrine download contents

Copy the Doctrine folder and the Doctrine.php file and put it inside the include path of your PHP installation. If you remember how we set up Zend Framework from the last tutorial, it’s most likely the same folder you placed the Zend library files in.

Doctrine library with Zend library in PHP's include_path

Doctrine library with Zend library in PHP’s include_path

Now we’re ready to integrate it with our Zend application! Begin by opening application.ini again and adding the following configuration inside the [development : production] block:

;Doctrine settings<br />resources.doctrine.connection_string = "mysql://[replace with db username]:[replace with db password]@localhost/thenextsocial"<br />resources.doctrine.models_path = APPLICATION_PATH "/models"<br />resources.doctrine.generate_models_options.pearStyle = true<br />resources.doctrine.generate_models_options.generateTableClasses = true<br />resources.doctrine.generate_models_options.generateBaseClasses = true<br />resources.doctrine.generate_models_options.classPrefix = "Model_"<br />resources.doctrine.generate_models_options.baseClassPrefix = "Base_"<br />resources.doctrine.generate_models_options.baseClassesDirectory = <br />resources.doctrine.generate_models_options.classPrefixFiles = false<br />resources.doctrine.generate_models_options.generateAccessors = false<br />

Now that we have our configuration set up, open the application’s Bootstrap.php file. You’ll find this inside the thenextsocial/application folder.

The Bootstrap.php

The Bootstrap.php lets us initialize any resources we might use in our application. Basically, all resources we need to instantiate should be placed here. We’ll dive into this in more detail later in the series, but for now, all you need to know is that the format for methods here are like this:

protected function _initYourResource(){//Do your resource setup here}

Inside the Bootstra.php file, add the following code to initialize Doctrine with the application:

<?phpclass Bootstrap extends Zend_Application_Bootstrap_Bootstrap{public function _initDoctrine(){//require the Doctrine.php filerequire_once 'Doctrine.php';//Get a Zend Autoloader instance$loader = Zend_Loader_Autoloader::getInstance();//Autoload all the Doctrine files$loader->pushAutoloader(array('Doctrine', 'autoload'));//Get the Doctrine settings from application.ini$doctrineConfig = $this->getOption('doctrine');//Get a Doctrine Manager instance so we can set some settings$manager = Doctrine_Manager::getInstance();//set models to be autoloaded and not included (Doctrine_Core::MODEL_LOADING_AGGRESSIVE)$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING,Doctrine::MODEL_LOADING_CONSERVATIVE);//enable ModelTable classes to be loaded automatically$manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES,true);//enable validation on save()$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE,Doctrine_Core::VALIDATE_ALL);//enable sql callbacks to make SoftDelete and other behaviours work transparently$manager->setAttribute(Doctrine_Core::ATTR_USE_DQL_CALLBACKS,true);//not entirely sure what this does :)$manager->setAttribute(Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE,true);        //enable automatic queries resource freeing$manager->setAttribute(Doctrine_Core::ATTR_AUTO_FREE_QUERY_OBJECTS,true);//connect to database$manager->openConnection($doctrineConfig['connection_string']);//set to utf8$manager->connection()->setCharset('utf8');return $manager;}protected function _initAutoload(){// Add autoloader empty namespace$autoLoader = Zend_Loader_Autoloader::getInstance();$resourceLoader = new Zend_Loader_Autoloader_Resource(array('basePath' => APPLICATION_PATH,'namespace' => '','resourceTypes' => array('model' => array('path' => 'models/','namespace' => 'Model_')),));// Return it so that it can be stored by the bootstrapreturn $autoLoader;}}

The setup I’ve done here is based on a script I found in the past on https://dev.juokaz.com, which was maintained by Juozas Kaziukenas, one of the team members at the Doctrine project. Sadly, the blog has already been shut down, so I won’t be able to link to it anymore. Also, take note that we have another method called _initAutoload(). This basically initializes the Zend Autoloader, which will autoload all the generated models inside the models folder. This saves us the hassle of having to include these files one by one.

Next, we need to setup the Doctrine CLI script that we’ll use to auto-generate Models from the database. Go back to the thenextsocial folder and create a folder called scripts. Inside, create a file named doctrine-cli.php and put the following inside:

<?php/** * Doctrine CLI script * * @author Juozas Kaziukenas ([email protected]) */define('APPLICATION_ENV', 'development');define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));set_include_path(implode(PATH_SEPARATOR, array(    realpath(APPLICATION_PATH . '/../library'),    './',    get_include_path(),)));require_once 'Zend/Application.php';// Create application, bootstrap, and run$application = new Zend_Application(    APPLICATION_ENV,    APPLICATION_PATH . '/configs/application.ini');$application->getBootstrap()->bootstrap('doctrine');// set aggressive loading to make sure migrations are workingDoctrine_Manager::getInstance()->setAttribute(    Doctrine::ATTR_MODEL_LOADING,    Doctrine_Core::MODEL_LOADING_AGGRESSIVE);$options = $application->getBootstrap()->getOptions();$cli = new Doctrine_Cli($options['doctrine']);$cli->run($_SERVER['argv']);

Go back inside to your models folder and delete the User model files we have there (if you want, you can move it to another location first, but it shouldn’t be inside the folder). Next, open up your command prompt (or terminal), cd to the scripts folder and type in the following command:

php doctrine-cli.php

You should see something like this:

Expected Doctrine CLI output

Expected Doctrine CLI output

If everything worked out, let’s start creating some models! Type in the following:

php doctrine-cli.php generate-models-db

You should now see the following output:

Generating Models using Doctrine CLI

Generating Models using Doctrine CLI

If you did, check out your models folder again and you should see some brand new User and UserSettings models that have been generated by Doctrine!

Generated Models!

Generated Models!

If you open the files, you won’t see much inside. Most of the code for the models are abstracted by the Doctrine library. By extending the Doctrine_Record class, we have available to us a lot of prebuilt methods from the library. Open IndexController.php again and replace the old test code with the following:

public function indexAction(){// action body$this->view->current_date_and_time = date('M d, Y - H:i:s');$user = new Model_User();$user->email = '[email protected]';$user->password = 'test';$user->salt = sha1(time());$user->date_created = date('Y-m-d H:i:s');$user->save();}

Once done, go back to https://thenextsocial.local. If the page loads, check your MySQL table and you should see that a brand new User record has been inserted!

User record via Doctrine ORM

User record via Doctrine ORM

Now, let’s try some more complicated stuff — retrieving an existing User via prebuilt Doctrine methods and updating it. Update the code so it looks like this:

public function indexAction(){// action body$this->view->current_date_and_time = date('M d, Y - H:i:s');$user = Doctrine_Core::getTable('Model_User')->findOneByEmailAndPassword('[email protected]', 'test');$user->password = 'new_password';$user->save();}

The findOneByEmailAndPassword() method is a convenience method prebuilt by Doctrine to make it easier to select one row from the database. The great thing about it is you can mix-and-match table columns in the method. For example, you can call something like findOneByIdAndNameAndPasswordAndSalt() and it will still work!

Updating a User record via Doctrine ORM

Updating a User record via Doctrine ORM

There’s a whole lot more we can do now that we use Doctrine ORM for the application’s Model implementation. Stuff like the Doctrine Query Language (DQL), taking advantage of Model relationships and generating Models from YAML. For the remainder of the series, we’ll be using Doctrine ORM for the Model implementation of the application, so you’ll actually be learning two things in the series instead of just one! Score!


Conclusion

By now, you should be able to know the following:

  • What are “Models”
  • Connecting your Zend application to a database
  • How the application.ini works
  • The DAO Design Pattern
  • Creating Models using the ZF CLI tool
  • Where to download the Doctrine ORM and how to install it
  • Integrating the Doctrine ORM with your Zend application
  • The Bootstrap.php
  • Generating Models using the Doctrine CLI tool
  • Basic usage of Models generated with Doctrine

Now that you know how to implement the Models in an Zend Framework powered application, you have the knowledge to create dynamic websites. Try to play around with the application, create some new Controllers and Models that read, update, save and delete from the database.

In our next tutorial, we’ll learn about some often used components of the Zend Framework library, the Zend_Auth and Zend_Acl components and build TheNextSocial‘s authentication system!

Until then, stay tuned, and remember that all the code used here is available on TheNextSocial’s GitHub repository!



Original Link: http://feedproxy.google.com/~r/nettuts/~3/RDcY670yR9E/

Share this article:    Share on Facebook
View Full Article

TutsPlus - Code

Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.

More About this Source Visit TutsPlus - Code