Book Image

FuelPHP Application Development Blueprints

By : Sebastien Drouyer
Book Image

FuelPHP Application Development Blueprints

By: Sebastien Drouyer

Overview of this book

Table of Contents (13 chapters)
FuelPHP Application Development Blueprints
Credits
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

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 name

  • ATTR_1, ATTR_2… are the model's attribute names

  • TYPE_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.

Note

You might have noticed that we used the 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 the Inflector::pluralize method. The table will contain the specified columns (name, still_here), the id column, and also created_at and updated_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).

Note

It is important to note that the 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 the DBUtil::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.

    Tip

    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 the up 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.

    Tip

    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:

http://my.app/monkey

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.

Note

Just to be perfectly clear, requesting the URL http://my.app/monkey/view/ID will load the monkey instance with id = ID.

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:

http://my.app/monkey

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, and description 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');

Note

You might have read 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
    )
);
?>

Note

In the code above, the first parameter is the name attribute of the checkbox. The second parameter is the value attribute of the checkbox. The third parameter determines whether the checkbox is checked or not. You can notice that, when we create a new monkey (and therefore no monkey is set), the checkbox will be checked by default. You are recommended to read the official documentation for more information about the 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)

Tip

Our solution works, but, in most cases, hard-coding a default value is not a good idea. When indicating a default value, for a request parameter or a configuration item, the best is to define this value inside a centralized configuration file and load it from there. Always avoid hard-coding constants, even for default values.

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:

http://my.app/

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 at APPPATH/config/routes.php.