VI. Building your first application
Now that we had a quick overview of the FuelPHP framework, let's build our first tiny application.
Suppose that you are a zoo manager and you want to keep track of the monkeys you are looking after. For each monkey, you want to save the following:
- Its name
- If it is still in the zoo
- Its height
- A description input where you can enter custom information
You want a very simple interface with the following five major features:
- You want to create a new monkey
- You want to edit existing ones
- You want to list all monkeys
- You want to view a detailed file for each monkey
- You want to delete monkeys from the system
The preceding five major features, very common in computer applications, are part of the Create, Read, Update and Delete (CRUD) basic operations. This is a perfect example to use the oil utility to generate a scaffold. Oil will quickly generate for us the controllers, models, views, and migrations to handle our monkeys. All we will have to do, then, is to refine the generated code and adapt it to our needs.
Database configuration
As we will store our monkeys into a MySQL database, it is time to configure FuelPHP to use our local database. If you open fuel/app/config/db.php
, all you will see is an empty array, but, as we demonstrated it in the FuelPHP basics section, this configuration file is merged to fuel/app/config/ENV/db.php
, ENV
being the current FuelPHP's environment, which in that case is development
.
You should, therefore, open fuel/app/config/development/db.php
:
<?php
//...
return array(
'default' => array(
'connection' => array(
'dsn' => 'mysql:host=localhost;dbname=fuel_dev',
'username' => 'root',
'password' => 'root',
),
),
);
This is the generated default configuration, which you should adapt to your local configuration, particularly the database name (currently set to fuel_dev
), the username, and password. You must create the database of your project manually.
Scaffolding
Now that the database configuration is set, we will be able to generate a scaffold. We will use the generate
feature of the oil utility.
Open the command-line utility and go to your website root directory. To generate a scaffold for a new model, you will need to enter the following line:
php oil generate scaffold/crud MODEL ATTR_1:TYPE_1 ATTR_2:TYPE_2 ...
where:
MODEL
is the model nameATTR_1
,ATTR_2
… are the model's attribute namesTYPE_1
,TYPE_2
… are attribute types
In our case, it should be as follows:
php oil generate scaffold/crud monkey name:string still_here:bool
height:float description:text
Here we are telling oil to generate a scaffold for the monkey
model with the following attributes:
name
: The name of the monkey. Its type is string and the associated MySQL column type will be VARCHAR(255).still_here
: Whether or not the monkey is still in the facility. Its type is boolean and the associated MySQL column type will be TINYINT(1).height
: Height of the monkey. Its type is float and the associated MySQL column type will be FLOAT.description
: Description of the monkey. Its type is text and the associated MySQL column type will be TEXT.
You can do much more using the oil generate feature, such as generating models, controllers, migrations, tasks, packages, and so on. We will see some of these later in the book, but you are recommended to take a look at the official documentation at http://fuelphp.com/docs/packages/oil/generate.html (It can be accessed through the FuelPHP website by navigating to DOCS | TABLE OF CONTENTS | Oil | Generate)
When you press Enter, you will see the following lines appear:
Creating migration: APPPATH/migrations/001_create_monkeys.php
Creating model: APPPATH/classes/model/monkey.php
Creating controller: APPPATH/classes/controller/monkey.php
Creating view: APPPATH/views/monkey/index.php
Creating view: APPPATH/views/monkey/view.php
Creating view: APPPATH/views/monkey/create.php
Creating view: APPPATH/views/monkey/edit.php
Creating view: APPPATH/views/monkey/_form.php
Creating view: APPPATH/views/template.php
Oil has generated for us nine files, which are as follows:
- A migration file, containing all the necessary information to create the model's associated table
- The model
- A controller
- Five view files and a template file
We will take a closer look at these files in the next sections.
scaffold/crud
command, and, if you read the official documentation, we could have typed only scaffold
. This is because two types of scaffold can be generated: scaffold/crud
, which uses simple models, and scaffold/orm
alias scaffold
, which uses the orm
models. Since using FuelPHP's native ORM was out of the scope of this chapter, and we didn't have to use complex model features such as relations, we chose to use scaffold/crud
.Migrating
One of the generated files was APPPATH/migrations/001_create_monkeys.php
. It is a migration file and contains the required information to create our monkey table. Notice that the name is structured as VER_NAME
, where VER
is the version number and NAME
is the name of the migration.
If you execute the following command line:
php oil refine migrate
All migration files that have not yet been executed will be executed from the oldest version to the latest version (001, 002, 003, and so on). Once all migration files are executed, oil will display the latest version number.
Once executed, if you take a look at your database, you will observe that not one but two tables have been created:
monkeys
: As expected, a table has been created to handle your monkeys. Notice that the table name is the plural version of the word we typed for generating the scaffold; such a transformation was internally done using theInflector::pluralize
method. The table will contain the specified columns (name
,still_here
), theid
column, and alsocreated_at
andupdated_at
. These columns store the time an object was created and updated, and are added by default each time you generate your models. It is possible to not generate them with the--no-timestamp
argument.migration
: This table is automatically created the first time you execute migrations. It keeps track of the migrations that were executed. If you look into its content, you will see that it already contains one row; this is the migration you just executed. You can notice that the row does not only indicate the name of the migration, but also a type and a name. This is because migration files can be placed at many places such as modules or packages (see Chapter 3, Building a Blog Application).
migration
table is not the only location where FuelPHP keeps track of the already executed migrations. This information is also stored in fuel/app/config/ENV/migrations.php
, ENV
being FuelPHP's environment. If you decide to edit the migration
table, you might want to also edit or delete this file, as it might prevent the execution of your migrations.The refine migrate
feature of oil allows you to have much more control on migrations than simply executing all the new ones. For instance, you can also revert to a previous version using the following command line:
php oil refine migrate:down
Or revert to a specified version using the following command line:
php oil refine migrate --version=3
Or even choose which modules or packages you want to update using the --modules
or --package
arguments. To have a complete overview, you are recommended to take a look at the official documentation at http://fuelphp.com/ docs/general/migrations.html (It can be accessed through the FuelPHP website by navigating to DOCS | TABLE OF CONTENTS | FuelPHP | Migrations)
But how do migration files allow such complex manipulations? Let's open our migration file located at APPPATH/migrations/001_create_monkeys.php
to find out. You should see the following:
<?php
namespace Fuel\Migrations;
class Create_monkeys
{
public function up()
{
\DBUtil::create_table('monkeys', array(
'id' => array(
'constraint' => 11,
'type' => 'int',
'auto_increment' => true,
'unsigned' => true
),
'name' => array(
'constraint' => 255,
'type' => 'varchar'
),
'still_here' => array(
'type' => 'bool'
),
'height' => array(
'type' => 'float'
),
'description' => array(
'type' => 'text'
),
'created_at' => array(
'constraint' => 11,
'type' => 'int',
'null' => true
),
'updated_at' => array(
'constraint' => 11,
'type' => 'int',
'null' => true
),
), array('id'));
}
public function down()
{
\DBUtil::drop_table('monkeys');
}
}
The file contains a class named Create_monkeys
that has the following two methods:
up
: This method defines how to update your data structure. Note that this migration file creates the monkey table using theDBUtil::create_table
method, but you could perfectly execute a handmade SQL request to do that. Though migrations are generally used to update your database, you can also use them to update custom data files or old configuration files.In some cases, if you want to implement your own migrations, you might find the idea of using your application's methods (in models or helpers) attractive. Though it can allow you to limit your code duplication, it is not recommended. This is because, for compatibility reasons, the migration files are intended to stay in your application indefinitely, whereas your application's code can evolve a lot. Therefore, by changing or deleting a method in your application, you might unexpectedly break some migration files (that use this method) without even noticing it, making the future installation of your application complicated.down
: This method defines how to cancel all changes that were made by theup
method. Suppose you realize that the feature was a mistake and you want to revert to an older version: this is when this method will be executed. In our case, the method simply deletes the monkey table.If the information contained in the table is important, it might be a good idea to instead move the table, for instance, to an archive database. A human mistake could have disastrous consequences otherwise.
The migration files are a powerful tool and their usefulness increase tenfold as the number of instances and the number of developers working on the same project rise. Using them from scratch is always a good decision.
Using your application
Now that we have generated the code and migrated the database, our application is ready to be used. You might have noticed during the generation that a controller was created at APPPATH/classes/controller/monkey.php
and that the route configuration file was not changed, meaning that the controller must be accessible through the default URL.
Let's request, then, the URL http://my.app/monkey.
As you can notice, this web page is intended to display the list of all monkeys, but since none have been added, the list is empty:
Then, let's add a new monkey by clicking on the Add new Monkey button. The following web page should appear:
You can enter your monkey's information here. There are, however, several inconsistencies:
- All fields are required, meaning that you can't leave any field empty, otherwise errors will be triggered preventing you from adding the monkey. This is not what we might want for the description field.
- Though you can enter anything you want in the Height field without triggering any error, if you enter anything other than a float, it will be replaced by 0. We might want to trigger an error in such a case.
- Still here can only have two values: 0 or 1 (false or true). Though the type of the associated database column is correct, the generated form uses a standard input where we might want a checkbox.
The form is certainly not perfect, but it is a great start. All we will have to do is refine the code a little bit.
Once you have added several monkeys, you can again take a look at the listing page as follows:
Again, this is a great start, though we might want to refine it a little bit: display Yes and No instead of 1 and 0, respectively, for the Still here column, and remove the Description column because there might be too much text to display.
Each item on the list has three associated actions: View, Edit, and Delete.
Let's first click on View:
Again this is a great start, though we will also refine this web page.
You can return back to the listing by clicking on Back or edit the monkey by clicking on Edit. Accessed from either the listing page or the view page, it will display the same form as when creating a new monkey, except that the form will be prefilled of course.
Finally, if you click on Delete, a confirmation box will appear to prevent any miss clicking:
Refining the application
Now that we took a look at our interface, let's refine our application so that it becomes more user-friendly. In this section, we will explore the files that have been generated by oil and try to adapt them to our needs.
Refining the monkey listing
During the previous section, two small issues bothered us for the monkey's listing:
- We wanted more explicit values than 0 and 1 for the
Still here
column - We wanted to remove the
Description
column
We know that the list appears when requesting the following URL:
You have probably noticed that in this URL we indicated a controller, but no action. It is important to know that, by default and without any routing configuration involved, this URL is equivalent to http://my.app/monkey/index.
So, in fact, we are calling the index
action of the monkey
controller. If we open the generated controller at APPPATH/classes/controller/monkey.php
, we will read the following:
<?php
class Controller_Monkey extends Controller_Template{
//...
}
First, you can notice that Controller_Monkey
extends Controller_Template
instead of Controller
, as we saw before in Controller_Welcome
. Controller_Template
is an extension of Controller
that adds template support. The idea is that most of the time your web pages will have the same layout: the headers, footers, and menus generally stay the same, regardless of the web pages you are in. Templates allow you to achieve this by limiting the code duplication.
By default, Controller_Template
is associated with the APPPATH/views/ template.php
template that was generated by oil. If you open this file, you will see that it generates the HTML code around the page content. You will also probably notice that it prints the $title
and $content
variables. We will find out how to set their values by exploring the index
action. If you go back to the Monkey controller, the action_index
method should contain the following:
public function action_index()
{
$data['monkeys'] = Model_Monkey::find_all();
$this->template->title = "Monkeys";
$this->template->content = View::forge('monkey/index', $data);
}
The first line stores all the monkeys' instances into the $data['monkeys']
variable. In a general manner, MODEL::find_all()
returns all a model's instances, but it is definitely not the only method that retrieve instances. These methods will be discussed more thoroughly in Chapter 2, Building a To-do List Application.
The second and third lines set the $title
and $content
variables displayed in the template file. If you change the second line by $this->template->title = "My monkeys";
and then refresh the web page, you will see that its title has changed accordingly.
The third line sets the $content
variable to a view instance that, from what we have observed in the previous sections, executes the view file located at APPPATH/views/monkey/index.php
with the $monkey
variable set to all monkeys' instances. Let's open this view file. You should see the following:
<h2>Listing Monkeys</h2>
<br>
<?php if ($monkeys): ?>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Still here</th>
<th>Height</th>
<th>Description</th>
<th></th>
</tr>
</thead>
<tbody>
<?php foreach ($monkeys as $item): ?>
<tr>
<td><?php echo $item->name; ?></td>
<td><?php echo $item->still_here; ?></td>
<td><?php echo $item->height; ?></td>
<td><?php echo $item->description; ?></td>
<td>
<?php /* Action buttons */ ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p>No Monkeys.</p>
<?php endif; ?><p>
<?php /* Add new Monkey button */ ?>
</p>
We have found where the table is displayed, so it is time to make our changes.
First, remove the Description
column by removing the following:
<th>Description</th>
and
<td><?php echo $item->description; ?></td>
Then, let's refine how the Still here
attribute is displayed by replacing the following:
<td><?php echo $item->still_here; ?></td>
by
<td><?php echo $item->still_here ? 'Yes' : 'No'; ?></td>
The Still here column should now display Yes and No instead of 1 and 0, respectively.
Refining the monkey detailed view
On the list, when clicking on an item's View link, a detailed view of the monkey appears. We would like to change two details here:
- As in the previous section, display more explicit values for the
Still here attribute - Currently, if you save a monkey with a multiline description, it is displayed on one line only
First, if you are on a detailed view page, you can notice that the URL is similar to http://my.app/monkey/view/1
This means we are calling the view
action of the monkey
controller with the first and only parameter set to 1. The view
action is quite similar to the index
action, as you can see in the following snippet:
public function action_view($id = null)
{
is_null($id) and Response::redirect('monkey');
$data['monkey'] = Model_Monkey::find_by_pk($id);
$this->template->title = "Monkey";
$this->template->content = View::forge('monkey/view', $data);
}
The first line simply checks if the parameter of the action (associated to the $id
variable) is actually set, and otherwise redirects the user (using the Response::redirect
method) to the listing page.
The second line stores the monkey with ID $id
into the $data['monkey']
variable. The find_by_pk
(pk for primary key) method of a model finds one of its instances by its primary key. As we explained earlier, models' methods will be discussed more thoroughly in Chapter 2, Building a To-do List Application.
The third and fourth lines, as in the previous section, set the template variables. The template content is set to the view located at APPPATH/views/monkey/view.php
.
<h2>Viewing #<?php echo $monkey->id; ?></h2>
<p>
<strong>Name:</strong>
<?php echo $monkey->name; ?></p>
<p>
<strong>Still here:</strong>
<?php echo $monkey->still_here; ?></p>
<p>
<strong>Height:</strong>
<?php echo $monkey->height; ?></p>
<p>
<strong>Description:</strong>
<?php echo $monkey->description; ?></p>
<?php /* Edit button */ ?> |
<?php /* Back button */ ?>
It is time to do some changes.
Replace:
<?php echo $monkey->still_here; ?>
By:
<?php echo $monkey->still_here ? 'Yes' : 'No'; ?>
And replace:
<?php echo $monkey->description; ?>
By:
<div><?php echo nl2br($monkey->description); ?></div>
Allowing an empty description
One of the issues we pointed out previously, is that the description field is required, though we want to be able to enter an empty value.
First, open your browser and request the following URL:
Click on the Add a new Monkey button, and you can see you are redirected to http://my.app/monkey/create
If you take a look at the page source, you will find that the form's action
attribute is actually the same URL:
<form class="form-horizontal" action="http://my.app/monkey/create"
accept-charset="utf-8" method="post">
It means that whether we are opening the monkey's creation form or submitting it, we will always call the create
action of the monkey
controller. We should then read how this action is implemented:
public function action_create()
{
if (Input::method() == 'POST')
{
$val = Model_Monkey::validate('create');
if ($val->run())
{
// Saves the model (out of this chapter scope)
}
else
{
Session::set_flash('error', $val->error());
}
}
$this->template->title = "Monkeys";
$this->template->content = View::forge('monkey/create');
}
As you can notice, the action is able to know whether or not it is accessed through a POST request by using Input::method()
. You are recommended to take a look at the official documentation of the Input
class at http://fuelphp.com/docs/classes/input.html (It can be accessed through the FuelPHP website by navigating to DOCS | TABLE OF CONTENTS | Core | Input)
Model_Monkey::validate('create')
returns an object that seems to define whether or not the object can be saved (depending on what $val->run()
returns). This is a method from the Monkey model, so we should look into it. Open APPPATH/classes/model/monkey.php
:
<?php
class Model_Monkey extends Model_Crud
{
protected static $_table_name = 'monkeys';
public static function validate($factory)
{
$val = Validation::forge($factory);
$val->add_field('name', 'Name', 'required|max_length[255]');
$val->add_field('still_here', 'Still Here', 'required');
$val->add_field('height', 'Height', 'required');
$val->add_field('description', 'Description', 'required');
return $val;
}
}
The file contains the Model_Monkey
class that extends Model_Crud
and allows us to handle the monkey instances.
First, you can notice the $_table_name
static attribute that defines the table name where the objects are saved (here, all our monkeys are saved into the monkeys
table).
And then there is the validate
static method we are looking for. It returns a Validation
object, that in our case will check that:
- The
name
attribute is not empty and its length is less than 255 characters still_here
,height
, anddescription
are not empty
For more detail about this class, you are recommended to read the official documentation at http://fuelphp.com/docs/classes/validation/validation.html (It can be accessed through the FuelPHP website by navigating to DOCS | TABLE OF CONTENTS | Core | Validation | Introduction)
In our case, simply comment or remove the following line:
$val->add_field('description', 'Description', 'required');
Session::set_flash
several times in the Controller_Monkey
controller and Session::get_flash
several times in the template. Session flash variables have a very limited life span and are generally used to store temporary information, such as notices or errors displayed to the user.Checking whether the height is a float
It is now easy to check if the height is a float. As we know that monkeys are generally not taller than 4 feet, we can even add a numerical constraint. In the validate
method of Model_Monkey
, replace the following line:
$val->add_field('height', 'Height', 'required');
by
$val->add_field(
'height',
'Height',
'required|numeric_between[0,6]'
);
Using a checkbox instead of an input for the still_here attribute
This change will be a bit more complex. First, still in the validate
method of Model_Monkey
, remove the following line as we won't need this validation:
$val->add_field('still_here', 'Still Here', 'required');
Now, if you go back to our create
action in Controller_Monkey
(located at APPPATH/classes/controller/monkey.php
), you will see that the template content is set to the view located at APPPATH/views/monkey/create.php
. If you look at the file content, it is pretty simple:
<h2>New Monkey</h2>
<br>
<?php echo render('monkey/_form'); ?>
<p><?php echo Html::anchor('monkey', 'Back'); ?></p>
For your information, the render
method is an alias of View::render
, and in this case equivalent to View::forge
. This illustrates that it is possible to render views inside other views. It can be convenient to prevent code repetition; the view located at APPPATH/views/monkey/edit.php
also renders the same view (monkey/_form
), and this makes sense since the forms displayed are exactly the same, whether you create a new monkey or edit an existing one.
Since we want to edit the form to replace the still_here
input by a checkbox, open the view located at APPPATH/views/monkey/_form.php
and replace the following lines:
<?php
echo Form::input(
'still_here',
Input::post(
'still_here',
isset($monkey) ? $monkey->still_here : ''
),
array(
'class' => 'col-md-4 form-control',
'placeholder' => 'Still here'
)
);
?>
By
<?php
echo Form::checkbox(
'still_here',
1,
Input::post(
'still_here',
isset($monkey) ? $monkey->still_here : true
)
);
?>
Form
class at http://fuelphp.com/docs/classes/form.html (It can be accessed through the FuelPHP website by navigating to DOCS | TABLE OF CONTENTS | Core | Form)Finally, you are probably aware that the still_here
POST attribute won't be defined if the checkbox is unchecked when submitting the form. Thus, we need to define a default value when retrieving the still_here
POST attribute, not only in the create
action but also in the edit
action. In both the methods, replace the following:
Input::post('still_here')
by
Input::post('still_here', 0)
Setting custom routes
Last but not least, we don't want to display FuelPHP's welcome screen when requesting the root URL, but instead the monkeys' listing. For doing that we will have to change the routes' configuration file located at APPPATH/config/routes.php
.
Replace:
'_root_' => 'welcome/index',
By:
'_root_' => 'monkey/index',
When requesting:
You should now see your monkey listing.
Removing useless routes and files
Now that our project is working as intended, it might be a good idea to clean it:
- Remove
APPPATH/classes/controller/welcome.php
as we don't need this controller anymore - Remove the
APPPATH/classes/presenter
folder - Remove the
APPPATH/views/welcome
folder - And remove the
_404_
,hello(/:name)?
,my/welcome/page
keys from the routes' configuration file located atAPPPATH/config/routes.php
.
This tutorial contains the first chapter of my book FuelPHP application development blueprints. I released this chapter freely so that you can have an idea of what the book looks like and get a first overview of the FuelPHP framework. Don't hesitate to comment or contact me if you have any question. You can buy the book on the Packt Publishing website, on Amazon, on BN.com and most internet book retailers.