The application we are going to create is a to-do list manager. The application will allow us to create to-do items and check them off. We'll also need the ability to edit and delete an item. As we are building a simple application, we need just four pages:
Page | Notes |
---|---|
Checklist homepage | This will display the list of to-do items. |
Add new item | This page will provide a form for adding a new item. |
Edit item | This page will provide a form for editing an item. |
Delete item | This page will confirm that we want to delete an item and then delete it. |
Each page of the application is known as an action, and actions are grouped into controllers within modules. Generally, related actions are placed into a single controller; for instance, a news controller might have actions of current
, archived
and view
.
We will store information about our to-do items in a database. A single table will suffice with the following fields:
Field name | Type | Null? | Notes |
---|---|---|---|
id | integer | No | Primary key, auto-increment |
title | varchar(100) | No | Name of the file on disk |
completed | tinyint | No | Zero if not done, one if done |
created | datetime | No | Date that the to-do item was created |
We are going to use MySQL, via PHP's PDO driver, so create a database called mytasklist
using your preferred MySQL client, and run these SQL statements to create the task_item table and some sample data:
CREATE TABLE task_item (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
completed TINYINT NOT NULL DEFAULT '0',
created DATETIME NOT NULL,
PRIMARY KEY (id)
);
INSERT INTO task_item (title, completed, created)
VALUES ('Purchase conference ticket', 0, NOW());
INSERT INTO task_item (title, completed, created)
VALUES ('Book airline ticket', 0, NOW());
INSERT INTO task_item (title, completed, created)
VALUES ('Book hotel', 0, NOW());
INSERT INTO task_item (title, completed, created)
VALUES ('Enjoy conference', 0, NOW());
Note that if you have Zend Studio, you can use the built-in Database Connectivity features. This if found in the Database Development perspective (Window | Open Perspective | Other | Database Development menu item) and further details are in the Zend Studio manual <http://files.zend.com/help/Zend- Studio/content/data_tools_platform.htm>.
We will create all our code within a module called Checklist. The Checklist
module will, therefore, contain our controllers, models, forms and views, along with specific configuration files.
We create our new Checklist
module in Zend Studio. In the PHP Explorer on the left, right click on the MyTaskList project folder and choose New -> Zend Framework Item. Click on Zend Module and press Next. The Source Folder should already be set to /MyTaskList/module
. Enter Checklist as the Module name and Task as the Controller name and then press Finish:
The wizard will now go ahead and create a blank module for us and register it with the Module Manager's application.config.php
. You can see what it has done in the PHP Explorer view under the module
folder:
As you can see the Checklist module has separate directories for the different types of files we will have. The config
folder contains configuration files, and the PHP files that contain classes within the Checklist
namespace live in the src/Checklist
directory. The view
directory also has a sub-folder called checklist
for our module's view scripts, and the tests
folder contains PHPUnit test files.
As mentioned earlier, a module's Module
class contains methods that are called during the start-up process and is also used to register listeners that will be triggered during the dispatch process. The Module
class created for us contains three methods: getAutoloaderConfig()
, getConfig()
and onBootstrap()
which are called by the Module Manager during start-up.
Our getAutoloaderConfig()
method returns an array that is compatible with ZF2's AutoloaderFactory
. It is configured for us with both a classmap file (autoload_classmap.php
) and a standard autoloader to load any files in src/Checklist
according to the PSR-0 <https://github.com/php-fig/fig- standards/blob/master/accepted/PSR-0.md> rules .
Classmap autoloading is faster, but requires adding each new class you create to the array within the autoload_classmap.php file, which slows down development. The standard autoloader, however, doesn't have this requirement and will always load a class if its file is named correctly. This allows us to develop quickly by creating new classes when we need them and then gain a performance boost by using the classmap autoloader in production. Zend Framework 2 provides bin/classmap_generator.php
to create and update the file.
The getConfig()
method in Checklist\Module
is called by the Module Manager to retrieve the configuration information for this module. By tradition, this method simply loads the config/module.config.php
file which is an associative array. In practice, the Module Manager requires that the returned value from getConfig()
be a Traversable
, which means that you can use any configuration format that Zend\Config
supports. You will find, though, that most examples use arrays as they are easy to understand and fast.
The actual configuration information is placed in config/module.config.php
. This nested array provides the key configuration for our module. The controllers
sub-array is used to register this module's controller classes with the Controller Service Manager which is used by the dispatcher to instantiate a controller. The one controller that we need, TaskController
, is already registered for us.
The router
sub-array provides the configuration of the routes that are used by this module. A route is the way that a URL is mapped to a to a particular action method within a controller class. Zend Studio's default configuration is set up so that a URL of /checklist/foo/bar
maps to the barAction()
method of the FooController
within the Checklist
module. We will modify this later.
Finally, the view_manager
sub-array within the module.config.php
file is used to register the directory where our view files are with the View sub-system. This means that within the view/checklist
sub-folder, there is a folder for each controller. We have one controller, TaskController
, so there is a single sub-folder in view/checklist
called task
. Within this folder, there are separate .phtml
files which contain the specific HTML for each action of our module.
The onBootstrap()
method in the Module
class is the easiest place to register listeners for the MVC events that are triggered by the Event Manager. Note that the default method body provided by Zend Studio is not needed as the ModuleRouteListener
is already registered by the Application
module. We do not have to register any events for this tutorial, so go ahead and delete the entire OnBootstrap()
method.
As we have four pages that all apply to tasks, we will group them in a single controller called TaskController
within our Checklist
module as four actions. Each action has a related URL which will result in that action being dispatched. The four actions and URLs are:
Page | URL | Action |
---|---|---|
Homepage | /task |
index |
Add new task | /task/add |
add |
Edit task | /task/edit |
edit |
Delete task | /task/delete |
delete |
The mapping of a URL to a particular action is done using routes that are defined in the module's module.config.php
file. As noted earlier, the configuration file, module.config.php
created by Zend Studio has a route called checklist
set up for us.
The default route provided for us isn't quite what we need. The checklist
route is defined like this:
module/Checklist/src/config/module.config.php:
'router' => array(
'routes' => array(
'checklist' => array(
'type' => 'Literal',
'options' => array(
'route' => '/task',
'defaults' => array(
'__NAMESPACE__' => 'Checklist\Controller',
'controller' => 'Task',
'action' => 'index',
),
),
'may_terminate' => true,
'child_routes' => array(
'default' => array(
'type' => 'Segment',
'options' => array(
'route' => '/[:controller[/:action]]',
),
),
),
),
This defines a main route called checklist
, which maps the URL /task
to the index action of the Task controller and then there is a child route called default
which maps /task/{controller name}/{action name}
to the {action name} action of the {controller name} controller. This means that, by default, the URL to call the add action of the Task controller would be /task/task/add
. This doesn't look very nice and we would like to shorten it to /task/add
.
To fix this, we will rename the route from checklist
to task
because this route will be solely for the Task controller. We will then redefine it to be a single Segment
type route that can handle actions as well as just route to the index action
Open module/Checklist/config/module.config.php
in Zend Studio and change the entire router section of the array to be:
module/Checklist/src/config/module.config.php:
'router' => array(
'routes' => array(
'task' => array(
'type' => 'Segment',
'options' => array(
'route' => '/task[/:action[/:id]]',
'defaults' => array(
'__NAMESPACE__' => 'Checklist\Controller',
'controller' => 'Task',
'action' => 'index',
),
'constraints' => array(
'action' => '(add|edit|delete)',
'id' => '[0-9]+',
),
),
),
),
),
We have now renamed the route to task and have set it up as a Segment
route with two optional parameters in the URL: action
and id
. We have set a default of index
for the action
, so that if the URL is simply /task
, then we shall use the index action in our controller.
The optional constraints
section allow us to specify regular expression patterns that match the characters that we expect for a given parameter. For this route, we have specified that the action
parameter must be either add, edit or delete and that the id
parameter must only contain numbers.
The routing for our Checklist module is now set up, so we can now turn our attention to the controller.
In Zend Framework 2, the controller is a class that is generally called {Controller name}Controller
. Note that {Controller name}
starts with a capital letter. This class lives in a file called {Controller name}Controller.php
within the Controller
directory for the module. In our case that's the module/Checklist/src/Checklist/Controller
directory. Each action is a public function within the controller class that is named {action name}Action
. In this case {action name}
should start with a lower case letter.
Note that this is merely a convention. Zend Framework 2's only restrictions on a controller is that it must implement the Zend\Stdlib\Dispatchable
interface. The framework provides two abstract classes that do this for us: Zend\Mvc\Controller\ActionController
and Zend\Mvc\Controller\RestfulController
. We'll be using the AbstractActionController
, but if you're intending to write a RESTful web service, AbstractRestfulController
may be useful.
Zend Studio's module creation wizard has already created TaskController
for us with two action methods in it: indexAction()
and fooAction()
. Remove the fooAction()
method and the default “Copyright Zend” DocBlock comment at the top of the file. Your controller should now look like this:
module/Checklist/src/Checklist/Controller/TaskController.php:
namespace Checklist\Controller;
use Zend\Mvc\Controller\AbstractActionController;
class TaskController extends AbstractActionController
{
public function indexAction()
{
return array();
}
}
This controller now contains the action for the home page which will display our list of to-do items. We now need to create a model-layer that can retrieve the tasks from the database for display.
It is time to look at the model section of our application. Remember that the model is the part that deals with the application's core purpose (the so-called "business rules") and, in our case, deals with the database. Zend Framework does not provide a Zend\Model
component because the model is your business logic and it's up to you to decide how you want it to work.
There are many components that you can use for this depending on your needs. One approach is to have model classes represent each entity in your application and then use mapper objects that load and save entities to the database. Another is to use an Object-relational mapping (ORM) technology, such as Doctrine or Propel. For this tutorial, we are going to create a fairly simple model layer using an entity and a mapper that uses the Zend\Db
component. In a larger, more complex, application, you would probably also have a service class that interfaces between the controller and the mapper.
We already have created the database table and added some sample data, so let’s start by creating an entity object. An entity object is a simple PHP object that represents a thing in the application. In our case, it represents a task to be completed, so we will call it TaskEntity
.
Create a new folder in module/Checklist/src/Checklist
called Model
and then right click on the new Model
folder and choose New -> PHP File. In the New PHP File dialog, set the File Name to TaskEntity.php
as shown and then press Finish.
This will create a blank PHP file. Update it so that it looks like this:
module/Checklist/src/Checklist/Model/TaskEntity.php:
The Task
entity is a simple PHP class with four properties with getter and setter methods for each property. We also have a constructor to fill in the created
property. If you are using Zend Studio rather than Eclipse PDT, then you can generate the getter and setter methods by right clicking in the file and choosing Source -> Generate Getters and Setters <http://files.zend.com/help /Zend-Studio-10/zend-studio.htm#creating_getters_and_setters.htm>.
We now need a mapper class which is responsible for persisting task entities to the database and populating them with new data. Again, right click on the Model folder and choose New -> PHP File and create a PHP file called TaskMapper.php
. Update it so that it looks like this:
module/Checklist/src/Checklist/Model/TaskMapper.php:
Within this mapper class we have implemented the fetchAll()
method and a constructor. There's quite a lot going on here as we're dealing with the Zend\Db
component, so let's break it down. Firstly we have the constructor which takes a Zend\Db\Adapter\Adapter
parameter as we can't do anything without a database adapter. Zend\Db\Sql
is an object that abstracts SQL statements that are compatible with the underlying database adapter in use. We are going to use this object for all of our interaction with the database, so we create it in the constructor.
The fetchAll()
method retrieves data from the database and places it into a HydratingResultSet
which is able to return populated TaskEntity
objects when iterating. To do this, we have three distinct things happening. Firstly we retrieve a Select
object from the Sql
object and use the order()
method to place completed items last. We then create a Statement
object and execute it to retrieve the data from the database. The $results
object can be iterated over, but will return an array for each row retrieved but we want a TaskEntity
object. To get this, we create a HydratingResultSet
which requires a hydrator and an entity prototype to work.
The hydrator is an object that knows how to populate an entity. As there are many ways to create an entity object, there are multiple hydrator objects provided with ZF2 and you can create your own. For our TaskEntity
, we use the ClassMethods
hydrator which expects a getter and a setter method for each column in the resultset. Another useful hydrator is ArraySerializable
which will call getArrayCopy()
and populate()
on the entity object when transferring data. The HydratingResultSet
uses the prototype design pattern when creating the entities when iterating. This means that instead of instantiating a new instance of the entity class on each iteration, it clones the provided instantiated object. See http://ralphschindler.com/2012/03/09/php- constructor-best-practices-and-the-prototype-pattern for more details.
Finally, fetchAll()
returns the result set object with the correct data in it.
In order to always use the same instance of our TaskMapper
, we will use the Service Manager to define how to create the mapper and also to retrieve it when we need it. This is most easily done in the Module
class where we create a method called getServiceConfig()
which is automatically called by the Module Manager and applied to the Service Manager. We'll then be able to retrieve it in our controller when we need it.
To configure the Service Manager we can either supply the name of the class to be instantiated or create a factory (closure or callback) method that instantiates the object when the Service Manager needs it. We start by implementing getServiceConfig() and write a closure that creates a TaskMapper
instance. Add this method to the Module
class:
module/Checklist/Module.php:
class Module
{
public function getServiceConfig()
{
return array(
'factories' => array(
'TaskMapper' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$mapper = new TaskMapper($dbAdapter);
return $mapper;
}
),
);
}
// ...
Don't forget to add use Checklist\Model\TaskMapper;
to the list of use statements at the top of the file.
The getServiceConfig()
method returns an array of class creation definitions that are all merged together by the Module Manager before passing to the Service Manager. To create a service within the Service Manager we use a unique key name, TaskMapper
. As this has to be unique, it's common (but not a requirement) to use the fully qualified class name as the Service Manager key name. We then define a closure that the Service Manager will call when it is asked for an instance of TaskMapper
. We can do anything we like in this closure, as long as we return an instance of the required class. In this case, we retrieve an instance of the database adapter from the Service Manager and then instantiate a TaskMapper
object and return it. This is an example of the Dependency Injection pattern at work as we have injected the database adapter into the mapper. This also means that Service Manager can be used as a Dependency Injection Container in addition to a Service Locator.
As we have requested an instance of Zend\Db\Adapter\Adapter
from the Service Manager, we also need to configure the Service Manager so that it knows how to instantiate a Zend\Db\Adapter\Adapter
. This is done using a class provided by Zend Framework called Zend\Db\Adapter\AdapterServiceFactory
which we can configure within the merged configuration system. As we noted earlier, the Module Manager merges all the configuration from each module and then merges in the files in the config/autoload
directory (*.global.php
and then *.local.php
files). We'll add our database configuration information to global.php
which you should commit to your version control system.You can then use local.php
(outside of the VCS) to store the credentials for your database.
Open config/autoload/global.php
and replace the empty array with:
config/autoload/global.php:
return array(
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter' =>
'Zend\Db\Adapter\AdapterServiceFactory',
),
),
'db' => array(
'driver' => 'Pdo',
'dsn' => 'mysql:dbname=mytasklist;hostname=localhost',
'driver_options' => array(
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
),
),
);
Firstly, we provide additional Service Manager configuration in the service_manager section, This array works exactly the same as the one in getServiceConfig()
, except that you should not use closures in a config file as if you do Module Manager will not be able to cache the merged configuration information. As we already have an implementation for creating a Zend\Db\Adapter\Adapter
, we use the factories
sub-array to map the key name of Zend\Db\Adapter\Adapter
to the string name of the factory class (Zend\Db\Adapter\AdapterServiceFactory
') and the Service Manager will then use ZendDbAdapterAdapterServiceFactory to instantiate a database adapter for us.
The Zend\Db\Adapter\AdapterServiceFactory
object looks for a key called db
in the configuration array and uses this to configure the database adapter. Therefore, we create the db
key in our global.php
file with the relevant configuration data. The only data that is missing is the username and password required to connect to the database. We do not want to store this in the version control system, so we store this in the local.php
configuration file, which, by default, is ignored by git.
Open config/autoload/local.php
and replace the empty array with:
config/autoload/local.php:
return array(
'db' => array(
'username' => 'YOUR_USERNAME',
'password' => 'YOUR_PASSWORD',
),
);
Obviously you should replace YOUR_USERNAME and YOUR_PASSWORD with the correct credentials.
Now that the Service Manager can create a TaskMapper
instance for us, we can add a method to the controller to retrieve it. Add getTaskMapper()
to the TaskController
class:
module/Checklist/src/Checklist/Controller/TaskController.php:
public function getTaskMapper()
{
$sm = $this->getServiceLocator();
return $sm->get('TaskMapper');
}
We can now call getTaskMapper()
from within our controller whenever we need to interact with our model layer. Let's start with a list of tasks when the index action is called.