In this tutorial we will create a complete Zend Framework 2 application and explore some of the features it provides. We will create a sticky note application based on the styling provide by Codepen. This tutorial is similar to the brilliant inventory tutorial provided by Rob Allen. The major difference is that we will spend more time on styling and we will handle the Create, Read, Update, and Delete (CRUD) operations dynamically using jQuery Ajax. You can download the complete source code from Github.

Download Source

Before we get started, we assume that you are familiar with some of the concepts discussed in how to build your first web application. With that said, let’s dive right into it.

Install Zend

  1. Unpack the downloaded file to your file system.
  2. Add the path to the Zend Library to the PHP include path.

Starting Point

Luckily the people from Zend provide a Skeleton application as starting point. Therefore, you do not need to create the project setup from scratch.

So go ahead and create your application’s base folder and run the following commands.

cd /my-project/dir
git clone git://github.com/zendframework/ZendSkeletonApplication.git
cd ZendSkeletonApplication
php composer.phar self-update
php composer.phar install

Or

git clone git://github.com/zendframework/ZendSkeletonApplication.git --recursive

After cloning the application you can change the directory name to StickyNotes.

Adding a Virtual Host

To setup a virtual host you can visit Apache’s website. Since we are building an application we will have 3 environments (development, staging, and production).

Development
This is our working copy. This version of the code contains the latest version and all the experimental features. Developers directly interact with this copy and continuously change, modify and test the latest integration and features.
Staging
The staging environment contains the next release candidate and is usually one version ahead of the production version. In this version final tests are performed and almost no new features are introduced. If you are working with a team, this environment is where your code and your project team code are combined and validated for production.
Production
This is the stable released version of the software available to the end-users. This version does not change until the next iteration of the software is proven to be stable in the staging environment and ready to be released. Production environment is the actual environment in which your system will run after deployment.

To make our application aware of which development environment it is in we use `SetEnv` variable in our VirtualHost deceleration. This is helpful to us as the database connections and other environment variables may be different across our different application environments.

<VirtualHost *:80>
    ServerName stickynotes.local
    DocumentRoot my/project/dir/public
    SetEnv APPLICATION_ENV "development" // make sure to set the application environment
</VirtualHost>

Before We Start

We will change some of the default setting to match our application. We can modify the header and footer section of our global layout. Within these sections we can change the page title, the copyright statement, the applications favicon and etc. So, let’s change the page title.

// change line 6
// /module/Application/view/layout/layout.phtml

<?php echo $this->headTitle('ZF2 '. $this->translate('Sticky Notes'))->setSeparator(' - ')->setAutoEscape(false)  // change Skeleton Application to Sticky Notes ?> 

// and change line 32

<a class="brand" href="<?php echo $this->url('home') ?>"><?php echo $this->translate('Sticky Notes') // change Skeleton Application to Sticky Notes ?></a>

Modules

In order to handle all the operations needed to create our StickyNotes Application we need to create a new module. In Zend Framework every module has its own directory and hosts all the files and functions relative to the respective module. Therefore, each module has its own MVC and are independent of other modules. Let’s create the directory structure for StickyNotes module.

/module
    /StickyNotes 
        /config
        /src 
            /StickyNotes 
   	        /Controller 
    	        /Form 
    	        /Model 
        /view 
            /sticky-notes 
                /sticky-notes

After setting up the directory hierarchy to host our module we need to make Zend Framework 2 aware of  the existence of our new module. Hence we add the new module to the config/application.config.php.

'modules' => array(
    'Application',
    'StickyNotes', // add this line
),

Now Zend Framework 2 will look into module/StickyNotes/Module.php for two functions namely getAutoloaderConfig and getConfig to load our new module into our application. Let’s create this file and setup the auto-loader to let our application know which files to load.

//  module/StickyNotes/Module.php
namespace StickyNotes;

class Module
{
    public function getAutoloaderConfig()
    {
        return array(
            'Zend\Loader\ClassMapAutoloader' => array(
                __DIR__ . '/autoload_classmap.php',
            ),
            'Zend\Loader\StandardAutoloader' => array(
                'namespaces' => array(
                    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
                ),
            ),
        );
    }

    public function getConfig()
    {
        return include __DIR__ . '/config/module.config.php';
    }
}

Each module’s namespace is the directory name of that module. However, the framework allows us to have more namespaces per module. If we look at the getAutoloaderConfig we see that it tries to load autoload_classmap.php. The application will look into this file to know which files to load. However, if this file returns an empty array it will force the application to use the fallback class map (StandardAutoloader) provided by Zend Framework 2.

// module/StickyNotes/autoload_classmap.php
return array();

This way the autoloader will fallback to StandardAutoloader whenever it tries to look for a file in our module.

Next we need to create /config/module.config.php as the getConfig function of the Module class includes and returns this file. /config/module.config.php defines the controllers and view managers that our module supports. This file must return an array containing two keys `controllers` and `view_manager`.

// module/StickyNotes/config/module.config.php:
return array(
    'controllers' => array(
        'invokables' => array(
            'StickyNotes\Controller\StickyNotes' => 'StickyNotes\Controller\StickyNotesController',
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'stickynotes' => __DIR__ . '/../view',
        ),
    ),
);

Controllers

A controller is the glue between the model and the view. It provides the functionality that allows us to change the view’s presentation of the model and enables us to send commands through the view to modify the model object. In Zend Framework 2 each controller lives in the module/ModuleName/src/ModuleName/Controller. Each controller must contain a set of actions for different view components of the controller. Zend Framework 2 naming conventions dictate that a controller’s name must be {ControllerName}Controller and each action must be named {action_name}Action. Our StickyNotesController will contain 4 actions. An indexAction to list all our sticky notes, an addAction to add new notes, removeAction to remove selected note and update action to save the updated content of the selected sticky note.

Since we will be handling the CRUD operations of the StickyNotes Apllication dynamically using ajax we must enable our controller to be able to return JSON objects from its action functions.

// module/StickyNotes/src/StickyNotes/Controller/StickyNotesController.php:

namespace StickyNotes\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class StickyNotesController extends AbstractActionController {
    public function indexAction() {
    }

    public function addAction(){
    }

    public function removeAction() {
    }

    public function updateAction(){
    }
}

Now that we have created our controller let’s add a router to the StickyNotes module’s config file to be able to navigate through our various actions.

 array(
        'invokables' => array(
            'StickyNotes\Controller\StickyNotes' => 'StickyNotes\Controller\StickyNotesController',
        ),
    ),
    // add this section
    'router' => array(
        'routes' => array(
            'stickynotes' => array(
                'type' => 'segment',
                'options' => array(
                    'route' => '/stickynotes[/:action][/:id]',
                    'constraints' => array(
                        'action' => '[a-zA-Z][a-zA-Z0-9_-]*',
                        'id' => '[0-9]+',
                    ),
                    'defaults' => array(
                        'controller' => 'StickyNotes\Controller\StickyNotes',
                        'action' => 'index',
                    ),
                ),
            ),
        ),
    ),
    'view_manager' => array(
        'template_path_stack' => array(
            'stickynotes' => __DIR__ . '/../view',
        ),
    ),
);

Database and Models

Our database consists of only one table which will contain all the information you need to store sticky notes. Let’s create a table called `stickynotes`. The stickynotes table will contain a unique `id` per note, the content of the note and the date it was created.

-- -----------------------------------------------------
-- Table `stickynotes`.`stickynotes`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `stickynotes`.`stickynotes` ;
CREATE TABLE IF NOT EXISTS `stickynotes`.`stickynotes` (
`id` INT NOT NULL AUTO_INCREMENT ,
`note` VARCHAR(255) NULL ,
`created` TIMESTAMP NOT NULL ,
PRIMARY KEY (`id`) ,
UNIQUE INDEX `id_UNIQUE` (`id` ASC) )
ENGINE = MyISAM;
-- -----------------------------------------------------
-- Data for table `stickynotes`.`stickynotes`
-- -----------------------------------------------------
START TRANSACTION;
USE `stickynotes`;
INSERT INTO `stickynotes`.`stickynotes` (`id`, `note`, `created`) VALUES (NULL, 'This is a sticky note you can type and edit.', NULL);
INSERT INTO `stickynotes`.`stickynotes` (`id`, `note`, `created`) VALUES (NULL, 'Let's see if it will work with my iPhone', NULL);
COMMIT;

Before we can use the data, we need to create a StickyNotes Object and create appropriate Database objects to map the StickyNotes Object to the database.

We will need to set the database credentials in the /config/autoload/global.php and /config/autoload/local.php files.

// config/autoload/global.php
return array(
    'db' => array(
        'driver'         => 'Pdo',
        'dsn'            => 'mysql:dbname=stickynotes;host=localhost',
        'driver_options' => array(
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ),
    ),
    'service_manager' => array(
        'factories' => array(
            'Zend\Db\Adapter\Adapter'
                    => 'Zend\Db\Adapter\AdapterServiceFactory',
        ),
    ),
);
// /config/autoload/local.php
// if it does not exists create it. It has already been excluded in .gitignore file
return array(
    'db' => array(
        'username' => 'Your_User_Name',
        'password' => 'Your_Password',
    ),
);

Next let’s create the StickyNote entity object. This is a simple object class that defines a StickyNote object. This class contains three variables (id, note, created) and the appropriate getters and setters.

// module/StickyNotes/src/StickyNotes/Model/Entity/StickyNote.php

namespace StickyNotes\Model\Entity;

class StickyNote {

    protected $_id;
    protected $_note;
    protected $_created;

    public function __construct(array $options = null) {
        if (is_array($options)) {
            $this->setOptions($options);
        }
    }

    public function __set($name, $value) {
        $method = 'set' . $name;
        if (!method_exists($this, $method)) {
            throw new Exception('Invalid Method');
        }
        $this->$method($value);
    }

    public function __get($name) {
        $method = 'get' . $name;
        if (!method_exists($this, $method)) {
            throw new Exception('Invalid Method');
        }
        return $this->$method();
    }

    public function setOptions(array $options) {
        $methods = get_class_methods($this);
        foreach ($options as $key => $value) {
            $method = 'set' . ucfirst($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            }
        }
        return $this;
    }

    public function getId() {
        return $this->_id;
    }

    public function setId($id) {
        $this->_id = $id;
        return $this;
    }

    public function getNote() {
        return $this->_note;
    }

    public function setNote($note) {
        $this->_note = $note;
        return $this;
    }

    public function getCreated() {
        return $this->_created;
    }

    public function setCreated($created) {
        $this->_created = $created;
        return $this;
    }

}

Now we create the TableGateway. This class loads the database table from our database and binds it with our StickyNotes object.

// module/StickyNotes/src/StickyNotes/Model/StickyNotesTable.php

namespace StickyNotes\Model;

use Zend\Db\Adapter\Adapter;
use Zend\Db\TableGateway\AbstractTableGateway;

class StickyNotesTable extends AbstractTableGateway {

    protected $table = 'stickynotes';

    public function __construct(Adapter $adapter) {
        $this->adapter = $adapter;
    }
}

Now we inject the database config into the StickyNotesTable through our Module class.

//  module/StickyNotes/Module.php

namespace StickyNotes;

use StickyNotes\Model\StickyNotesTable;

class Module {

    public function getAutoloaderConfig() {...}

    public function getConfig() {...}

    public function getServiceConfig() {
        return array(
            'factories' => array(
                'StickyNotes\Model\StickyNotesTable' => function($sm) {
                    $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                    $table = new StickyNotesTable($dbAdapter);
                    return $table;
                },
            ),
        );
    }
}

Now we can add the functionality to our database table class to perform operations on the database. We will add 4 new functions to this class. Fetchall returns all the entries in our database with a twist. The twist is that it fetches the rows based on the date in ascending order. This way the notes will always appear in the order they were added. GetStickyNote takes one parameter which is the id for a particular StickyNote entry in our database and returns a StickyNote object if the database entry exists. It returns false otherwise. SaveStickyNotes accepts a StickyNote object as its parameter and if the object exists in our database the object is overwritten other wise a new object is created. In both cases the object id is returned by the object. In case the function is unable to insert or update an object it returns false. And finally the removeStickyNote function deletes the entry matching the id passed to the function and returns a Boolean result.

// module/StickyNotes/src/StickyNotes/Model/StickyNotesTable.php

namespace StickyNotes\Model;

use Zend\Db\Adapter\Adapter;
use Zend\Db\TableGateway\AbstractTableGateway;
use Zend\Db\Sql\Select;

class StickyNotesTable extends AbstractTableGateway {

    protected $table = 'stickynotes';

    public function __construct(Adapter $adapter) {
        $this->adapter = $adapter;
    }

    public function fetchAll() {
        $resultSet = $this->select(function (Select $select) {
                    $select->order('created ASC');
                });
        $entities = array();
        foreach ($resultSet as $row) {
            $entity = new Entity\StickyNote();
            $entity->setId($row->id)
                    ->setNote($row->note)
                    ->setCreated($row->created);
            $entities[] = $entity;
        }
        return $entities;
    }

    public function getStickyNote($id) {
        $row = $this->select(array('id' => (int) $id))->current();
        if (!$row)
            return false;

        $stickyNote = new Entity\StickyNote(array(
                    'id' => $row->id,
                    'note' => $row->note,
                    'created' => $row->created,
                ));
        return $stickyNote;
    }

    public function saveStickyNote(Entity\StickyNote $stickyNote) {
        $data = array(
            'note' => $stickyNote->getNote(),
            'created' => $stickyNote->getCreated(),
        );

        $id = (int) $stickyNote->getId();

        if ($id == 0) {
            $data['created'] = date("Y-m-d H:i:s");
            if (!$this->insert($data))
                return false;
            return $this->getLastInsertValue();
        }
        elseif ($this->getStickyNote($id)) {
            if (!$this->update($data, array('id' => $id)))
                return false;
            return $id;
        }
        else
            return false;
    }

    public function removeStickyNote($id) {
        return $this->delete(array('id' => (int) $id));
    }

}

 Styling Zend Application

We are going to use the styling provided by Codepen. However, we must modify the code slightly for our application’s needs.

/** /public/css/StickyNotes.css */
@import url(https://fonts.googleapis.com/css?family=Gloria+Hallelujah);

* { box-sizing:border-box; }

body { background:url(https://subtlepatterns.com/patterns/little_pluses.png) #cacaca;}

#sticky-notes {
    float: left;
}
#create, textarea  {
    float:left;
    padding:25px 25px 40px;
    margin:0 20px 20px 0;
    width:250px;
    height:250px;
}

#create {
    user-select:none;
    padding:20px;
    border-radius:20px;
    text-align:center;
    border:15px solid rgba(0,0,0,0.1);
    cursor:pointer;
    color:rgba(0,0,0,0.1);
    font:220px "Helvetica", sans-serif;
    line-height:185px;
}

#create:hover { border-color:rgba(0,0,0,0.2); color:rgba(0,0,0,0.2); }

textarea {
    font:20px 'Gloria Hallelujah', cursive;
    line-height:1.5;
    border:0;
    border-radius:3px;
    background: linear-gradient(#F9EFAF, #F7E98D);
    background: -webkit-linear-gradient(#F9EFAF, #F7E98D);
    box-shadow:0 4px 6px rgba(0,0,0,0.1);
    overflow:hidden;
    transition:box-shadow 0.5s ease;
    transition:-webkit-box-shadow 0.5s ease;
    font-smoothing:subpixel-antialiased;
    max-width:520px;
    max-height:250px;
}
textarea:hover { box-shadow:0 5px 8px rgba(0,0,0,0.15); }
textarea:focus { box-shadow:0 5px 12px rgba(0,0,0,0.2); outline:none; }

.delete-sticky{
    float: left;
    margin: 5px 0 0 -35px;
    display: none;
}
a.delete-sticky {
    color: red;
}
.sticky-note{
    float: left;
}
.sticky-note:hover .delete-sticky{
    display: block;
}
.clear-both {
    clear: both;
}

To include this file in our project we must modify the layout.phtml

// /module/Application/view/layout/layout.phtml

        headLink(array('rel' => 'shortcut icon', 'type' => 'image/vnd.microsoft.icon', 'href' => $this->basePath() . '/images/favicon.ico'))
                ->prependStylesheet($this->basePath() . '/css/StickyNotes.css') // add this line
                ->prependStylesheet($this->basePath() . '/css/bootstrap-responsive.min.css')
                ->prependStylesheet($this->basePath() . '/css/style.css')
                ->prependStylesheet($this->basePath() . '/css/bootstrap.min.css')
        ?>

Actions

As we are going to handle the CRUD operation dynamically all of our actions will return JSON objects except for our indexAction. Before we start coding the action functions we will create another function to load StickyNotesTable and make it available to StickyNotesController.

// module/StickyNotes/src/StickyNotes/Controller/StickyNotesController.php:

namespace StickyNotes\Controller;

...

class StickyNotesController extends AbstractActionController {
    ...
    protected $_stickyNotesTable;
 public function getStickyNotesTable() {
        if (!$this->_stickyNotesTable) {
            $sm = $this->getServiceLocator();
            $this->_stickyNotesTable = $sm->get('StickyNotes\Model\StickyNotesTable');
        }
        return $this->_stickyNotesTable;
    }

}

The indexAction will return the list of all of our StickyNote objects to our view model. This will enable us to display all the notes in our application. The addAction will create a new sticky note, save it to the database and return its id in JSON format. The removeAction accepts an integer id and return whether the operation was successful or not. The update action takes the id and the updated content of the note. It simply replaces the old content with the new content and saves the object to the database.

 public function indexAction() {
        return new ViewModel(array(
                    'stickynotes' => $this->getStickyNotesTable()->fetchAll(),
                ));
    }
public function addAction() {
        $request = $this->getRequest();
        $response = $this->getResponse();
        if ($request->isPost()) {
            $new_note = new \StickyNotes\Model\Entity\StickyNote();
            if (!$note_id = $this->getStickyNotesTable()->saveStickyNote($new_note))
                $response->setContent(\Zend\Json\Json::encode(array('response' => false)));
            else {
                $response->setContent(\Zend\Json\Json::encode(array('response' => true, 'new_note_id' => $note_id)));
            }
        }
        return $response;
    }

    public function removeAction() {
        $request = $this->getRequest();
        $response = $this->getResponse();
        if ($request->isPost()) {
            $post_data = $request->getPost();
            $note_id = $post_data['id'];
            if (!$this->getStickyNotesTable()->removeStickyNote($note_id))
                $response->setContent(\Zend\Json\Json::encode(array('response' => false)));
            else {
                $response->setContent(\Zend\Json\Json::encode(array('response' => true)));
            }
        }
        return $response;
    }
    public function updateAction(){
        // update post
        $request = $this->getRequest();
        $response = $this->getResponse();
        if ($request->isPost()) {
            $post_data = $request->getPost();
            $note_id = $post_data['id'];
            $note_content = $post_data['content'];
            $stickynote = $this->getStickyNotesTable()->getStickyNote($note_id);
            $stickynote->setNote($note_content);
            if (!$this->getStickyNotesTable()->saveStickyNote($stickynote))
                $response->setContent(\Zend\Json\Json::encode(array('response' => false)));
            else {
                $response->setContent(\Zend\Json\Json::encode(array('response' => true)));
            }
        }
        return $response;
    }

add the code to the view to display the sticky notes

<?php
// /StickfNotes/view/sticky-notes/sticky-notes/index.phtml
?>

<div id="sticky-notes">
    <?php foreach($stickynotes as $stickynote):?>
    <div class="sticky-note">
        <textarea id="stickynote-<?php echo $stickynote->getId() ?>"><?php echo $stickynote->getNote() ?></textarea>
        <a href="#" id="remove-<?php echo $stickynote->getId(); ?>"class="delete-sticky">X</a>
    </div>
    <?php endforeach; ?>
    <div id="create">+</div>

</div>
<div class="clear-both"></div>

Let’s tie the functionality with jQuery

// /public/js/custom.js

jQuery(function($) {
    $("#create").on('click', function(event){
        event.preventDefault();
        var $stickynote = $(this);
        $.post("stickynotes/add", null,
            function(data){
                if(data.response == true){
                    $stickynote.before("<div class=\"sticky-note\"><textarea id=\"stickynote-"+data.new_note_id+"\"></textarea><a href=\"#\" id=\"remove-"+data.new_note_id+"\"class=\"delete-sticky\">X</a></div>");
                // print success message
                } else {
                    // print error message
                    console.log('could not add');
                }
            }, 'json');
    });

    $('#sticky-notes').on('click', 'a.delete-sticky',function(event){
        event.preventDefault();
        var $stickynote = $(this);
        var remove_id = $(this).attr('id');
        remove_id = remove_id.replace("remove-","");

        $.post("stickynotes/remove", {
            id: remove_id
        },
        function(data){
            if(data.response == true)
                $stickynote.parent().remove();
            else{
                // print error message
                console.log('could not remove ');
            }
        }, 'json');
    });

    $('#sticky-notes').on('keyup', 'textarea', function(event){
        var $stickynote = $(this);
        var update_id = $stickynote.attr('id'),
        update_content = $stickynote.val();
        update_id = update_id.replace("stickynote-","");

        $.post("stickynotes/update", {
            id: update_id,
            content: update_content
        },function(data){
            if(data.response == false){
                // print error message
                console.log('could not update');
            }
        }, 'json');

    });
});

and include custom.js in the application layout.phtml

<!-- Scripts -->  <?php  
echo $this->headScript()->prependFile($this->basePath() . '/js/html5.js', 'text/javascript', array('conditional' => 'lt IE 9',))  
    ->prependFile($this->basePath() . '/js/bootstrap.min.js')  
    ->prependFile($this->basePath() . '/js/jquery.min.js')  
    ->appendFile($this->basePath() . '/js/custom.js')  // add this line ?>;

To finish things we can route our home directory to StickyNotes module so when we access the application we are viewing all the notes in the home page. For this navigate to module/Application/config/module.config.php and modify

'defaults' => array(
'controller' => 'StickyNotes\Controller\StickyNotes', // change this line
'action' => 'index',
),

We are ready to test the completed Sticky Notes Application in a browser. You can download the complete source code from Github and view the completed project’s live demo.

This content was originally published here.