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.
MVC Architecture Diagram
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" resources.db.params.dbname = "thenextsocial" resources.db.params.host = "localhost" resources.db.params.username = "[your local database username]" resources.db.params.password = "[your local database password]" </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="http://nettuts.s3.amazonaws.com/1122_zend2/images/application_ini.png" alt="The application.ini file" title="The application.ini file" /> <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 “inherit” 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> [php] SetEnv APPLICATION_ENV development RewriteEngine On RewriteCond %{REQUEST_FILENAME} -s [OR] RewriteCond %{REQUEST_FILENAME} -l [OR] RewriteCond %{REQUEST_FILENAME} -d RewriteRule ^.*$ - [NC,L] RewriteRule ^.*$ index.php [NC,L]
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 = InnoDB CHARACTER 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 = InnoDB CHARACTER 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 touser.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:
The DAO Design Pattern
- 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 Creating a DbTable at thenextsocial/application/models/DbTable/User.php 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:
<?php class 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 Creating a model at thenextsocial/application/models/UserMapper.php 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 Creating a model at thenextsocial/application/models/User.php 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> [php] <?php class 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 user if( 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 exsception if( 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 object return $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:
<?php class Application_Model_User { //declare the user's attributes private $id; private $email; private $password; private $salt; private $date_created; //upon construction, map the values //from the $user_row if available public 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 model public 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_created if( !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 http://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!
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
Congratulations! You've successfully created your first ever Zend Framework Model!
The Doctrine ORM
Introduction
From the Doctrine ORM website, http://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 http://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
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
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
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 resources.doctrine.connection_string = "mysql://[replace with db username]:[replace with db password]@localhost/thenextsocial" resources.doctrine.models_path = APPLICATION_PATH "/models" resources.doctrine.generate_models_options.pearStyle = true resources.doctrine.generate_models_options.generateTableClasses = true resources.doctrine.generate_models_options.generateBaseClasses = true resources.doctrine.generate_models_options.classPrefix = "Model_" resources.doctrine.generate_models_options.baseClassPrefix = "Base_" resources.doctrine.generate_models_options.baseClassesDirectory = resources.doctrine.generate_models_options.classPrefixFiles = false resources.doctrine.generate_models_options.generateAccessors = false
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:
<?php class Bootstrap extends Zend_Application_Bootstrap_Bootstrap { public function _initDoctrine() { //require the Doctrine.php file require_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 bootstrap return $autoLoader; } }
The setup I've done here is based on a script I found in the past on http://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 working Doctrine_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
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
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!
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 http://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
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
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!
Comments