How I use CodeIgniter's MVC
Jim O'Halloran • September 6, 2007php codeigniter
There's always some discussion in the CodeIgniter community (especially the Forumsforums) about the MVC model. Usually these questions are asking about the "right" way to do something, or whether something should be in a model, the controller, or even a library. So what I wanted to do in this blog post is explain how I use the MVC framework offered by CodeIgniter and hopefully answer some of these questions.
I should note first up that I've developed a few applications in CodeIgniter at this point, and using these principles they all work well, maintain a clean separation between presentation and business logic, and are easy to maintain. What I'm presenting below is by no means the only way to write apps in CodeIgniter, there's no doubt going to be many many other approaches. However my approach does seem to be slightly different to some members of the CI community. For example, if you watch Derek Allard's Ajax video, you see Derek returning HTML from his model, something I'd avoid in my own apps. CI is a loose framework, you're free to use it however you like as long as it gets the job done for you. This is just my personal opinion.
Views are probably the easiest to describe. In my apps, anything that generates output that the user might see goes in a view. This includes all HTML, PDF's and emails. So if the user signs up for a service and we need to send them a confirmation email, the body of the email is generated via a view. All forms, HTML, and any other dynamic content should be created using a view. In this way we can give our views to a graphic designer without the possibility that they might accidentally change some important application logic.
I prefer to use the native CI view library and implement loops and conditionals using PHP code. The PHP code in the view should relate only to the presentation of the data. If we're producing an invoice, by all means include logic in your views to calculate a grand total for the invoice, and definitely use functions like number_format to display numeric values in a suitable format. If I find myself performing complicated tax calculations, or wanting to change data in my views, then I probably need to revisit the structure of the application. If you need to share bits of code between views, helpers and libraries may be the answer.
The Smarty templating engine makes some syntax (e.g. if statements) much cleaner, but has some downsides. The foreach construct in Smarty is a little awkward, and doing any sort of math (e.g. adding up a total) is downright painful. I've got a Smarty wrapper which I've used in place of the CI view library on some apps, but in general I've found it's simpler to use native PHP code.
Views: In short, use them wherever you're generating output, keep them to presentation logic only.
Models are an optional part of the CI framework, you can choose not to use them at all if you want. If you don;t use models, your controller methods will be used to perform all of the data access for your application. However, I find that using models makes my Controller code cleaner and easier to read and allows for better code reuse. I use a model wherever data storage and rereival is required. My models might contain methods such as load() and save() for working with single records, and functions like get_all(), get_active(), get_top_n() for working with sets of data.
Bulk methods would include things like a "get_all" method that returns an array of records in the table. This allows us to ensure the SQL statement always includes "WHERE status 'deleted'" in the query, and also to " cleanse" the data slightly before returning it to the controller. Data cleansing might include things like changing MySQL formatted dates to Unix timestamps, turning 0/1, t/f or Yes/No fields into proper booleans, or calculation of implied values. If I need to search the table for all records matching a particular criteria, that's a job for a model's bulk operation methods, so a controller which needed to get a list of overdue invoices would call $this->InvoiceModel->get_overdue(), the ligic of what actually constitutes an overdue invoice is contained within the model method itself.
Single record functions allow the model to load and save a single record. Usually there will be a load() method whigh takes a parameter I call $id_or_row. If an integer is supplied the load method will assume it's an ID number and query the requested record from the database. However an array containing the required record can also be supplied, and load method will load it directly into the object for editing. This gives us the ability to edit a single record from the result set of the get_all method without having to query the database for the same record again. Once a record has been load()'ed, its fields are available for editing via public properties on the model object itself. If additional logic is required when storing/retrieving a value I'll use a __get or __set method (PHP5 only) on the properties to accomplish this ( e.g. recalculating a total when the quantity changes). The load method will also cleanse data like the bulk methods before exposing it to the controller. Finally there's a save() method which will store the record in the database again. I prefer to use handwritten SQL statements with query bindings, but you can use Active Record if you prefer. The save() method is generally the only place you'll find SQL which modifies data in my apps, although the common exception to thisrule is a delete($id) method, which marks a record as deleted.
However you choose to structure your models, I'd advise keeping the data that's returned from the model as generic as possible. Lets say you have a generic search function. If that function returns an array of search results you can then pass that array to a view and format it for presentation. If you later want to present search results in a different way you can use the same search function in the model, but a different view and change the presentation that way.. If you model returned HTML directly, then you'd either need a second search() method or to add a parameter to the original to control the format of the results. Try and keep the input to a controller as generic as possible as well, let the controller map form fields onto a model's properties, that way you con use the model isn't tied directly to the a specific set of form fields.
Don't limit your thinking with models to just database operations. Any data storages/retrieval operation is a candidate for a model. Use them to wrap web service API's, and other sources of data as well. For example, lets say you have a series of images stored in a filesystem hierarchy that you need to manipulate, consider wrapping the file manipulation operations in a model. Later if you decide to switch to remote storage such as Amazon S3 you can re-code the model using the S3 API's and not have lo go searching through your entire application for file operations to change.
When storing/retrieving data from a database I generally have one model per table. Sometimes closely related tables may be combined into the one model e.g. If I have a "users" table and a "user_attributes" table with some additional configuration items for each user, I'll use one model for both tables.
Models: Use them for all data retrieval/storage operations, keep the return result as generic as possible. Arrays of raw (or slightly cleansed) data work well as return types fron model functions.
Controllers are the glue that binds the whole thing together. If we treat models as generic classes for data manipulation, and use views for the presentation of our data, controllers are used for pretty much anything else relating to the generation of a page. If functionality needs to be shared between Controllers but doesn't fit neatly into views or models, then we can consider the use of Libraries and Helpers.
I generally stick with CI's default /controller/method url structure unless I'm after a specific URL structure. This means that my controllers have functions for each page in a given section of the site. I have forms submit back to the same URL as the form itself loaded from, e.g. the /user/register form will submit to /user/register?. This means my controller methods generally follow this pattern:
- Determine whether the form has been submitted or needs to be displayed for the first time. If it's to be displayed, load the required data from the model and display the view.
- Validate form input. If validation fails, re-display the view with the submitted data and error message.
- If validation passes, load the record any existing model record, assign the form values to the properties of the model and call the save method, before redirecting to a results page.
The basic role of a controller in my applications is to process any input, load/save the appropriate models, select the appropriate view and display it. Handling input might include sending emails, using libraries to perform other operations, and so forth. In my applications, the controller classes often end up containing more code than the views or models.
Avoid sending output to the browser directly from the controller. If I find myself doing that it's probably a sign that I'm trying to use a controller for something that a View would be better suited. Equally, if I find myself wanting to query the database from a controller that's a sure sign that I should be adding functionality to my models instead.
Controllers: Use controllers like glue, they can retrieve data from models, and funnel it into a view.
I should note that there's a possible performance hit inherent in my approach. By retrieving the data in a model and cleansing it, then passing it through to a view I usually iterate through the dataset twice (once to load and cleanse, and a second time to display). If our model was to emit html we could avoid the second iteration. If we cut down on the number of long running loops in the application we can improve performance considerably. In my experience the time added by the second iteration is very small with most datasets and in my opinion the developer time savings offered by code reuse more than makes up for the additional execution time required to work in this manner. CI has some good benchmarking tools, so you can easily try both ways and compare the time differences for youself if you like.
What about Libraries and Helpers?
I treat libraries and helpers as pieces of generic code which any part of the application can call on to get something done. Libraries are simple classes, so if there's a group of methods which work together to acheive a particular task, a library is a good fit. Helpers on the other hand are just modules containing functions.
I've used Libraries to interact with Credit Card Payment gateways. In this situation I've had a series of functions to process a transaction and parse the result from the gateway. which I was able to wrap up neatly into a class and use as a library. You can also create a library class to wrap around an existing PHP object library and use it within CodeIgniter.
A series of independent functions with a related purpose could be grouped together in a helper. Helpers might be used to provide formatting or layout assistance in a view, or provide some additional functions for manipulating dates. For some examples of helpers, take a look at the credit card and Australian date helpers which I've posted on the CI Wiki.
Using the MVC framework in this way allows for maximum code reuse. By keeping models generic we can often find uses for the arrays returned by get_all or get_top_n type methods in a large number of different places (e.g. populating a select box on a form, listing out a series of records to edit), and this type of reuse is generally more awkward if the model returns a string or HTML. Keep all of your output (e.g. HTML, email text, and PDF generation) contained within a view, that way a designer can pretty up your application without breaking things, and finally use Controllers to glue everything together.
I hope this article has answered some of your questions about how I use CI. As I said in the beginning, this is what I do, it works for me, it results in very clean code and I like it. You might choose a totally different approach, others have so do whatever works for you unless of course one day you find youself working for me!