In this chapter, we will focus on the wall of the user. We will cover the changes we should make on the API level to provide the wall data as well as the code we need on the client side to display the user wall. As this is the first section of the social network we implement, we will also cover the basic access to the database. We will assume for now that the data is already in the database; we don't care how the data arrived there because our only job here is to show that data on the user wall. This means that the API will provide, for now, just the access to the data and the frontend will show the data. It is really simple and a good exercise to check how the frontend and API integrate and work together.
By the end of this chapter, you will know how the frontend and API work together to display a page on the social network. Also, you will understand how we manage to connect to the database and get the data we need.
As we are working with the API-centric architecture, we should first develop the API code in order to provide data to the clients. This is the general approach that we will follow through the book to develop any section of the social network.
In the previous chapter, we saw that ZF2 provides two controller types we can use. The first one called
AbstractActionController
will be used on the client side to develop the client itself. The second type, AbstractRestfulController
, is the one we will use on the API side to build a RESTful web service to provide access to the data and functionalities.
For the API level of the wall, we need to use a new endpoint. In this case the API will expose the data using the URI, /wall/:id
. Let's see a more formal description of this end-point and the functionality provided.
As you can see, get()
is the only method allowed at this endpoint. This method will return the data related to the profile as a JSON encoded string. In case the user is not found, this method will return an HTTP 404 error.
The API will always return a JSON encoded string as a result of the requests even if we are dealing with errors; this means that the API code will not have a view per se.
The first task we have to complete is the database work. We will need to create a table to store the data of the user. In order to create the table on the virtual machine, we need to connect to the database server and then create the table in the database. The first task is really easy because the MySQL port is open to the host machine.
Open your favorite SQL editor/manager and connect to the server using the IP of the VM. If you haven't made changes to the IP on the
Vagrantfile
, it should be 192.168.56.2
. As a username you should use root
and as the password you must use the one specified in Vagrantfile
. If you haven't changed it, it should also be root.
If you prefer to use the command-line interface to connect to the MySQL server, it will be like the following command line:
mysql –uroot –proot –h192.168.56.2
Once you connect, you should be able to see a database named sn
.
mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sn | | test | +--------------------+ 5 rows in set (0.00 sec)
Now let's create a table named users
inside the sn
database. This table will contain the following fields:
The create table statement is as follows:
CREATE TABLE `users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) DEFAULT NULL, `email` varchar(254) DEFAULT NULL, `password` binary(60) DEFAULT NULL, `avatar_id` int(11) unsigned DEFAULT NULL, `name` varchar(25) DEFAULT NULL, `surname` varchar(50) DEFAULT NULL, `bio` tinytext, `location` tinytext, `gender` tinyint(1) unsigned DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_username` (`username`), KEY `idx_email` (`email`(191)) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
As you can see, gender
is a
tinyint
value; in this case we need a convention to express male and female. We will use 1
for male and 0
for female.
Let's take an overview on how ZF2 manages to connect and retrieve data from a database.
There are three main components involved with databases: ZendDbAdapter
, ZendDbTableGateway
, and ZendDbRowGateway
.
This is the most important subcomponent of ZendDb
collection of objects. This object translates the queries we create on the code to vendor- and platform-specific code and takes care of all the differences between them. It creates an abstraction layer in the databases and provides a common interface.
When creating an instance of this object, we should provide minimum information such as the driver to use, the database name to connect, and username and password for the connection itself. After that we can use this object to query the database directly but, as mentioned, ZF2 provides two more objects that will ease the process.
This object is a representation of a table on the database and provides the usual operations we would like to do over a table such as, select()
, insert()
, delete()
, or update()
. This part of the library provides two implementations of TableGatewayInterface
: AbstractTableGateway
and TableGateway
. Two important things we should know about this component is that we can configure it to return each row in a RowGateway
object or provide a class that will act as an entity and will store the row data.
The other interesting functionality we should be aware of is the Features API. This internal API allows us to extend the functionality of the TableGateway
object without the need to extend the class and customize the behavior to meet our needs. As usual ZF2 provides a few built-in features we can take advantage of:
GlobalAdapterFeature
: This feature allows us to use a global adapter without the need to inject it into the TableGateway
object.MasterSlaveFeature
: This feature modifies the behavior of TableGateway
to use a specific master adapter for inserts, updates, and deletions and a slave adapter for select operations.MetadataFeature
: This feature gives us the ability to feed TableGateway
with the column information provided by a Metadata
object.EventFeature
: As you can imagine, this allows us to use events in TableGateway
and subscribe to events of the TableGateway
lifecycle.RowGatewayFeature
: This modifies the behavior of TableGateway
when executing select statements and forces the object to return a ResultSet
object. When iterating the ResultSet
object, we will be working with the RowGateway
objects.Following the concept of TableGateway
, RowGateway
represents a row inside the table of the database. This object also provides some methods related to the actions we usually make over a row, such as save()
and delete()
. As we mentioned before if you use RowGatewayFeature
over TableGateway
, the result of the select queries will be a ResultSet
object containing instances of this object. Each object will contain the data of the row it represents and we will be able to manipulate it, change it, and save it back to the database, everything using the same object.
Now we have a clear understanding about each object involved in the database connection, let's set up the code needed to actually connect to the database.
The first step is providing the information needed by ZendDbAdapter
. We will do this in two different config files: global.php
and local.php
. You can find them in the config
folder; the first one will be there and the second one should be created manually. The reason why we use two files is because local.php
will be ignored by git as stated in the .gitignore
file.
You can see in the folder that this allows us to put the general configuration that will be committed on the global.php
file, and keep the private data such as usernames and passwords on the local.php
file that will not be committed.
return array( 'db' => array( 'driver' => 'Pdo', 'dsn' => 'mysql:dbname=sn;host=localhost', 'driver_options' => array( PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES 'UTF8'' ), ), 'service_manager' => array( 'factories' => array( 'ZendDbAdapterAdapter' => 'ZendDbAdapterAdapterServiceFactory', ), ), );
This is the config data you should put in the global.php
file. Here, we are specifying a new entry called db
and inside that we define the options: driver
, dsn
, and driver_options
. As you can see, all the data is pretty straightforward.
When connecting to databases, ZF2 is able to use different drivers; the options available are Mysqli
, Sqlsrv
, Pdo
_Sqlite
, Pdo_Mysql
, and Pdo
. If you want to know more about the options available to connect to databases, you can check out the documentation at http://framework.zend.com/manual/2.0/en/modules/zend.db.adapter.html.
The second block of config is related to ServiceManager
. We are registering a new service called ZendDbAdapterAdapter
and we are pointing it to AdapterServiceFactory
of the Db
package.
Let's see the data we should add on the local.php
file. Keep in mind that our VM is running with the
skip-grant-tables
options so these credentials will not take effect but it's a good practice to define the data on the config file.
return array( 'db' => array( 'username' => 'root', 'password' => 'root', ), );
As you can notice we are extending the db
config section and adding the username and password for the db
connection.
The API response will always be text. That's the reason we are going to encode every response as JSON. There are a few ways to accomplish this task and we will see the most straightforward and easy. The View
component in Zend is created with flexibility in mind and provides a way to modify how the view works using strategies.
For our case, ZF2 provides us with a strategy called ViewJsonStrategy
. Basically this component attaches two methods to the EVENT_RENDERER
and EVENT_RESPONSE
events of the view and modifies the way the view works based on the data we send to the view. To make ViewJsonStrategy
work, we should return the JsonModel
objects on our controllers. The JsonModel
objects will be picked up by ViewJsonStrategyand
. We will take care of setting up the correct Content-Type
header. After that the component will use the provided JsonModel
object to get a serialized version of the data to be sent as a response.
To activate this strategy and start returning JSON data, we only have to add the following four lines to the global.php
file:
'view_manager' => array( 'strategies' => array( 'ViewJsonStrategy', ), ),
This will be the first module we create and is only going to contain the
TableGateway
object for the table that we just created on the database. Before starting to write the code, we need to create a new module with the following structure:
Now that we have the folder structure, the next step is to create a
TableGateway
object in order to be able to fetch the data from the database. Let's start by creating a file called UsersTable.php
inside the
Model
folder and put the following code inside:
<?php namespace UsersModel; use ZendDbAdapterAdapter; use ZendDbTableGatewayAbstractTableGateway; use ZendDbAdapterAdapterAwareInterface; class UsersTable extends AbstractTableGateway implements AdapterAwareInterface { protected $table = 'users'; public function setDbAdapter(Adapter $adapter) { $this->adapter = $adapter; $this->initialize(); } public function getByUsername($username) { $rowset = $this->select(array('username' => $username)); return $rowset->current(); } }
As this is the first TableGateway
we created, let's have a closer look at it. First thing we should do is declare the namespace where this class resides. After that we have to declare the components we are going to use inside the class using the use
keyword.
The class is extending AbstractTableGateway
and implementing the interface, AdapterAwareInterface
. This interface allows the dependency injector to set the adapter we have to use for the connections to the database. As we are extending AbtractTableGateway
, we have to define the name of the table we are representing; that's the job we do when we declare the protected variable, $table
. After that we arrive at the first method. This is just the implementation of the method stated in the interface. In this case we store the adapter on the local variable, and then we call the initialize()
method to set up the wiring on the object.
Finally, we define a custom method that we will use to get info from the table. We need to be able to retrieve users by their username and that's the job of the last method on this class. getByUsername()
uses the internal
select()
method by passing an array as an argument. This array basically is the where condition for the select statement and in this case we are just filtering by the username
column. As we are fetching the info of one user, instead of returning Rowset we call the
current()
method to return the first result. In theory we are going to have only one user with a specific username because we defined a unique index on the username
column.
Let's now have a look at the contents of the configuration file.
return array( 'di' => array( 'services' => array( 'UsersModelUsersTable' => 'UsersModelUsersTable' ) ), );
As we are just storing the TableGateway
object, the only entry on the config file is to define the availability of this object on the dependency injector.
As we have seen before in other classes, we have to define the current namespace and import the classes we are going to use; in this case the code is as follows:
namespace Users;
After that we define a class called Module
and then we jump to the first method, getConfig()
used by
ModuleManager
to retrieve the config file of the module. The method looks like the following code snippet:
public function getConfig() { return include __DIR__ . '/config/module.config.php'; }
As you can see we are just returning the array contained in the configuration file for the module. We will later explore the contents of this file.
We already saw that ZF2 is PSR-0 compliant and has its own auto-loading mechanism using the ZendLoader
components. The last method we declare on the Module
class is called getAutoloaderConfig()
and it's used to tell the autoloader where it can find the files for this module.
public function getAutoloaderConfig() { return array( 'ZendLoaderStandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__, ), ), ); }
Our next task is to create the module that will contain the code related to the user wall. Take a look at the following folder structure and recreate it inside the module folder:
Let's define the contents of the Module.php
file that should be placed at the root level of the Wall
module folder. Remember that this file is the configuration class used by ModuleManager
to get extra information about our modules.
As we saw before in the Users
module, we have to define the current namespace and import the classes we are going to use. In this case, the code looks exactly the same as the Users/Module.php
file and the only difference is the definition of the current namespace.
namespace Wall;
Let's now review the contents of the configuration file. The first section is related to the router and the routes we are listening to. We need to define a route to access the data and the controller/action that will fulfill the request. Let's see the following code snippet to know how it's configured:
'router' => array( 'routes' => array( 'wall' => array( 'type' => 'ZendMvcRouterHttpSegment', 'options' => array( 'route' => '/api/wall[/:id]', 'constraints' => array( 'id' => 'w+' ), 'defaults' => array( 'controller' => 'WallControllerIndex' ), ), ), ), ),
As you can see, this way of configuring the routes is very verbose, but it's also very convenient. We are defining a new route identified by the name, wall, of type ZendMvcRouterHttpSegment
and we define the structure of the route with an optional parameter, the id
of the user, which in our case will be the username. We also set up the constraint about what the id
should look like. Finally, in the defaults section, we define the controller in charge for requests sent to this URL.
Let's now dig in the last section of the configuration file that basically specifies the controllers available on this module.
'controllers' => array( 'invokables' => array( 'WallControllerIndex' => 'WallControllerIndexController' ), )
This section is as simple as defining an identifier for the controller and the class of the file containing the controller.
We have arrived at the last file of the API, the controller. Here, we put everything together and we fulfill the request with the data the clients need. As we said before, the controllers on the API side are based on AbstractRestfulController
. That means the action we will execute to fulfill the request is determined based on the type of requests.
The first thing is to define the namespace where this file resides, and then the components we will use in the class.
namespace WallController; use ZendMvcControllerAbstractRestfulController; use ZendViewModelJsonModel;
Then we proceed to create the class itself extending AbstractRestfulController
. The class will be called IndexController
.
In this controller, we have to accomplish two things: the first one is to implement the abstract methods of the parent class and the second one is to have access to the UsersTable
object. Let's see first how we get access to the UsersTable
object.
protected $usersTable; protected function getUsersTable() { if (!$this->usersTable) { $sm = $this->getServiceLocator(); $this->usersTable = $sm->get(UsersModelUsersTable'), } return $this->usersTable; }
We define a new protected variable inside the class that will hold the UsersTable
object. After that we create a new method called getUsersTable()
, which will get the UsersTable
object from ServiceManager
and will store it in the internal variable.
Now let's implement the methods of the parent class. We have a bunch of them as we defined in the requirements. As we said, this controller will only respond to the get()
method, the rest will not be used. So in that case we should follow the HTTP specification and issue a 405 code. Let's see how we deal with the methods that we don't use and then also see how we implement the get()
method.
public function getList() { $this->methodNotAllowed(); } public function create($data) { $this->methodNotAllowed(); } public function update($id, $data) { $this->methodNotAllowed(); } public function delete($id) { $this->methodNotAllowed(); } protected function methodNotAllowed() { $this->response->setStatusCode( endHttpPhpEnvironmentResponse::STATUS_CODE_405 ); }
As you see the unused methods call the methodNotAllowed()
method that will set the status code of the response to 405.
Let's see now how we tackle the get()
method.
public function get($username) { $usersTable = $this->getUsersTable(); $userData = $usersTable->getByUsername($username); if ($userData !== false) { return new JsonModel($userData->getArrayCopy()); } else { throw new Exception('User not found', 404); } }
The first thing we do is retrieve the UsersTable
object. After that, we call the getByUsername()
method we created before. If we have data, it means that the user exists so we proceed to return a
JsonModel
object containing a copy of the data. In case the user is not in our database, we throw an exception that will be caught by a listener, which we will implement in the next section.
Usually when we develop APIs, we use the HTTP status codes to report errors that we encounter while processing the request. But this is not enough in most cases and we end up creating our own objects to represent an error status on the API level. To solve this issue and standardize the way we respond to errors on API based on JSON or XML, two proposals are gaining track and they use the error media types, such as application/api-problem+json
and application/vnd.error+json
.
In this section we will build the foundation of this approach but we will cover it in more depth and implement it properly later to avoid overloading the chapter with a lot of new stuff.
We will implement the API-Error listener in a new module to have the code separated by responsibility. Let's first create the folder structure. Then we will jump to the code we should add and how it works.
Now, the first thing we should do is add the new modules to the application.config.php
file so the framework will know that they are available. In order to do that we need to modify the modules configuration like the following code snippet:
'modules' => array( 'Wall', 'Users', 'Common' ),
As you can see we just add as many entries as we have modules and they are called after the module name.
We have already seen that all the modules must have a file called Module.php
. As we discussed earlier, this file is used to get extra configuration and information about the module and it's used by ModuleManager
. For this module, we will add the usual code in our Module.php
to return the config file and configure the autoloader. Also, we will initialize our listener when the module is being bootstrapped.
public function getConfig() { return include __DIR__ . '/config/module.config.php'; } public function getAutoloaderConfig() { return array( 'ZendLoaderStandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__ ), ), ); } public function onBootstrap($e) { $app = $e->getTarget(); $services = $app->getServiceManager(); $events = $app->getEventManager(); $events->attach( $services->get('CommonListenersListener') ); }
The onBootstrap()
method is called when ModuleManager
is bootstrapping the module. We use this to configure the listener, adding it to EventManager
.
Now that we have the first bits of our module and the manager knows what to do with it, let's review the contents of the module.config.php
file. As you will see it is really simple.
return array( 'service_manager' => array( 'invokables' => array( 'CommonListenersApiProblemListener' => 'CommonListenersApiProblemListener', ), ), );
In this code, we are adding a new object as invokable to the service_manager
array. So, we will be able to retrieve it later.
Finally, let's have a look at the listener that will help us spot issues on the API and will modulate the standard response for API-Problem in the future.
As usual, the first thing to do is declare the namespace and the components we are going to use.
namespace CommonListeners; use ZendEventManagerAbstractListenerAggregate; use ZendEventManagerEventManagerInterface; use ZendMvcMvcEvent; use ZendViewModelJsonModel;
Right after that we will define a new class called ApiErrorListener
and we will make this class extend the AbstractListenerAggregate
class, which will allow the class to attach one or more listeners.
class ApiErrorListener implements AbstractListenerAggregate
Now we should track the events we are listening to in order to accomplish our need to add a property to the class to store them. Then we should define the attach()
method as required by the interface, ListenerAggregateInterface
. We have to attach our methods to the events we are interested in by calling the attach()
method of EventManager
and passing the event name, the method to call, and the priority as parameters.
public function attach(EventManagerInterface $events) { $this->listeners[] = $events->attach( MvcEvent::EVENT_RENDER, 'ApiErrorListener::onRender', 1000 ); }
Finally, let's see the code that inspects the response and determines if there is an error and format it.
public static function onRender(MvcEvent $e) { if ($e->getResponse()->isOk()) { return; } $httpCode = $e->getResponse()->getStatusCode(); $sm = $e->getApplication()->getServiceManager(); $viewModel = $e->getResult(); $exception = $viewModel->getVariable('exception'), $model = new JsonModel(array( 'errorCode' => $exception->getCode() ?: $httpCode, 'errorMsg' => $exception->getMessage() )); $model->setTerminal(true); $e->setResult($model); $e->setViewModel($model); $e->getResponse()->setStatusCode($httpCode); }
Let us go line by line in this big chunk of code. At the top we check if the response is marked as 200 OK. If it is, we don't have to continue worrying about errors; otherwise, we retrieve the status code and the exception variable from ViewModel
. We are assuming that if something goes wrong, an exception will be thrown. That's why on our controller if the user is not in the database, we throw an exception because it will be caught here.
With the data we extracted, we create a new JsonModel
object specifying errorCode
based on the code used in the exception; otherwise, we use the HTTP error and we also specify errorMsg
based on the text of the exception.
Then we use the setTerminal()
method of the JsonModel
object to set the terminal flag to true
to tell ZF2 to not wrap the returned model in a layout.
The last section of the method takes care of attaching the new JsonModel
object to the response overwriting the previous one and setting the corresponding status code.