Zend Framework 2 – Simple Web Application – CRUD using Ajax Tutorial

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. Download Zend Framework 2 from http://packages.zendframework.com/
  2. Unpack the downloaded file to your file system.
  3. 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(http://fonts.googleapis.com/css?family=Gloria+Hallelujah);

* { box-sizing:border-box; }

body { background:url(http://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.

96 thoughts on “Zend Framework 2 – Simple Web Application – CRUD using Ajax Tutorial”

  1. Hi,

    Thanks for the demo, There’s one thing i wasn’t able to figure it out. How come the folder under ‘view’ has to be ‘sticky-notes’? If i use /view/stickynotes/stickynotes instead of /view/sticky-notes/sticky-notes, there will be error 500.

    Hako

    1. Hi Hako,

      That is because our Module’s name is `StickyNotes` (CamelCase) which signifies the name consists of two words. Therefore, in our view we must separate the words by a hyphen. On the other hand, if the Module’s name was `Stickynotes` we could have simply used `stickynotes` as our directory name under view.

  2. Hi, when i try to hit the ‘+’ button it doesn’t do anything, and with the inspector i get this error:

    POST http://localhost/SN/public/stickynotes/add 500 (Internal Server Error)

    i am not using stickynotes as my default controller since i already know how to do that, i am just playing with it, anyway what could be the problem?

    1. @Benjamin,
      500 Internal Server Errors can happen for various reasons, more information about this error may be available in the server error log. That would be your first place to start debugging this.

  3. A few details:

    There is an error on the Module.php code block
    this> $table = new StickyNotesTable($dbAdapter);
    should be> $table = new Model\StickyNotesTable($dbAdapter);

    There is a misplaced ";" on layout.phtml, at the end of this line:

    this>  ->appendFile($this->basePath() . '/js/custom.js')  // add this line ?>; 
    should be> ->appendFile($this->basePath() . '/js/custom.js') ; // add this line ?>;

    Also there is an extra "/" at the beginning of the remove action url on the custom.js file
    this> $.post("/stickynotes/remove", {
    should be > $.post("stickynotes/remove", {

    Also, another way to change the title of the template could be just by editing en_us.po inside the languages folder, using the application poedit (opensource).

    1. Thanks for your comments Sergio.

      By adding use StickyNotes\Model\StickyNotesTable; on top of the Module.php we have loaded the StickyNotes\Models\StickyNotesTable namespace. Hence there is no need to write the entire path again.

      As for the misplaced ";" “The closing tag of a block of PHP code automatically implies a semicolon; you do not need to have a semicolon terminating the last line of a PHP block.” (Instruction separation, php.net)

      In the JS file we are using relative URLs. When a relative URL is clicked if there is no "/" in front of the link the the defined link will be added to the current pages link. So if we click on <a href="test">test</a> in example.com/example the resulting URL becomes example.com/example/test whereas <a href="/test">test</a> will result in example.com/test. Hence the "/" in front of the link makes sure we always call the correct page.

      Even though, you are right about changing the title with i18n methods it does not make sense to add unnecessary steps for our server to process.

  4. Thanks for this great tutorial. Finally a simple, but really helpful (and useful) tutorial which helped me to understand ZF2 better. 🙂

  5. Hi,
    Thanks for this tutorial.
    I have followed above steps, but when I run this, I am getting below error message.
    I am using XAMPP server on windows.
    Please advice.
    Thanks.
    Error Message:

    Fatal error: Call to a member function canCallMagicCall() on a non-object in C:\xampp\htdocs\zend\vendor\zendframework\zendframework\library\Zend\Db\TableGateway\AbstractTableGateway.php on line 474
    1. Obviously, the error isn’t in the library (or it shouldn’t be) but occurs when the TableGateWay calls your code. You should check the call stack (by enable error report, it should be enable in XAMP by default) to see where the exact function is called in your class which causes the error. I guess the cause is inside your Model.

      1. I am also getting this error and that function is not within any of my code… it is within: AbstractTableGateway.php on line 470

        Any thoughts?

        1. Try change this code:

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

          to:

              public function __construct(Adapter $adapter) {
                  $this->adapter = $adapter;
                  $this->initialize();
              }
          
  6. 404 - Controller:   StickyNotes\Controller\StickyNotes(resolves to invalid controller class or alias: StickyNotes\Controller\StickyNotes) 

    could post a picture of the structure of folders and files to compare and see if I made just right.
    more in the post is very good, I still have not learned that right.
    Thank you.

      1. 404 – Controller: StickyNotes\Controller\StickyNotes(resolves to invalid controller class or alias: StickyNotes\Controller\StickyNotes)
        dobble check the ‘invokables’ but still have the same problem

  7. Very nice tutorial but I get this error and dont understand why?
    I’m using the latest ZF2.1.1

    Catchable fatal error: Argument 1 passed to StickyNotes\Model\{closure}() must be an instance of StickyNotes\Model\Select, instance of Zend\Db\Sql\Select given, called in C:\wwwroot\zf2-tutorial\vendor\zendframework\zendframework\library\Zend\Db\TableGateway\AbstractTableGateway.php on line 190 and defined in C:\wwwroot\zf2-tutorial\module\StickyNotes\src\StickyNotes\Model\StickyNotesTable.php on line 18

    Help please?

    1. I removed the Select in Select $select and it works now so that was kind of easy but I dont really understand all the code yet. Really nice tutorial since I use a lot of ajax with my own jQuery plugin…gonna start learning all the parts in this now 😛

  8. Nice tutorial, much more focused than the official one. I noticed that you are not using the prototyping feature of the TableGateway and this makes your query code longer than it could be. Is there any reason for your approach?

    For example, the official tutorial fetchAll is just two lines:

    public function fetchAll()
        {
            $resultSet = $this->tableGateway->select();
            return $resultSet;
        }

    They achieve it by specifying a prototype resultset in the configuration (and implementing an exchangeArray function on the model):

                    'AlbumTableGateway' => function ($sm) {
                        $dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
                        $resultSetPrototype = new ResultSet();
                        $resultSetPrototype->setArrayObjectPrototype(new Album());
                        return new TableGateway('album', $dbAdapter, null, $resultSetPrototype);
  9. Hi Arian, thank you for the excellent tutorial.
    One question: there is a way to use php functions in js, like “$this->basePath()”. This would be very useful…

    1. You can use inline scripts to define js variables in your php code and later in your js file use these variables. The reason is because PHP executes on the server before the view is rendered.

      <script>
           var base_path = <? echo $this->basePath() ?>;
      </script>
      <script src="path_to_jsfile.js"></script>
  10. I am getting the following error:

    Fatal error: Class 'StickyNotes\Controller\StickyNotesController' not found in ...\vendor\ZF2\library\Zend\ServiceManager\AbstractPluginManager.php on line 170

    my module.config is as follows:

    array(
            'invokables' => array(
                'StickyNotes\Controller\StickyNotes' => 'StickyNotes\Controller\StickyNotesController',
            ),
        ),
        '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'
            ),
        ),
    );  
    

    My StickyNotesController.php is located in the following directory:

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

    Any help would be appreciated… Thanks

  11. I found my issue, just a typeo
    __NAMESPACE__ >= __DIR__ . '/src/' . __NAMESPACE__,
    changed to
    __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,

  12. It give error for me. The error is

    Fatal error: Class 'StickyNotes\Controller\ViewModel' not found in C:\wamp\www\ZF\module\StickyNotes\src\StickyNotes\Controller\StickyNotesController.php on line 28
    
    1. Make sure you load the `ViewModel` class correctly as `ViewModel` is a Zend library class which can be found under `Zend\view\Model\ViewModel`.

      namespace StickyNotes\Controller;
      
      use Zend\Mvc\Controller\AbstractActionController;
      use Zend\View\Model\ViewModel; // make sure this line exists
  13. Thanks for this tutorial…

    I can’t quite understand how you intend the JSON to render instead of the standard renderer. On my system, the home page renders fine, but when I try to add a note, the response is

    Zend\View\Exception\RuntimeException

    File:

    /usr/home/stickynotes/test/vendor/zendframework/zendframework/library/Zend/View/Renderer/PhpRenderer.php:457

    Message:

        Zend\View\Renderer\PhpRenderer::render: Unable to render template "sticky-notes/sticky-notes/add"; resolver could not resolve to a file
    1. From your error code it seems that your action is returning a ViewModel and the application is looking for the `view/sticky-notes/sticky-notes/add.phtml`. However, in the code above we are returning a response object with a JSON object as its content.

      
      public function addAction() {
              $response = $this->getResponse(); // get the response object
              $response->setContent(JsonObject); // set a Json Object as its content
              return $response; // return the server's response
      }
      1. Thanks… actually it turned out that I had a problem with SQL permissions in StickyNotesTable->saveStickyNote().

  14. Arian,

    Thanks for the great tutorial. I’m just not picking up PHP and the Zend Framework. Can you explain why all the modules other than Application have a redundant folder structure for their view? i.e. “view/sticky-notes/sticky-notes” and in the album tutorial it’s “view/album/album”. Why can’t it just be “view/album” or “view/sticky-notes”?

    Gary

    1. Gary,

      You can have multiple Controllers per module. For example, a Dashboard module can have different controllers for Users, Settings, Pages, …. For that reason, you would have `view/Dashboard/users`, `view/Dashboard/settings`, `view/Dashboard/pages` and so on.

      Each page of the application is known as an action and actions are grouped into controllers within modules. Hence, you would generally group related actions into a controller; for instance, a news controller might have actions of current, archived and view.

      Routing and controllers

      You can read more in Routing and Controller Plugins

  15. I am using your application from Github but it gives me error like this——

    Additional information:
    Zend\Db\Adapter\Exception\RuntimeException
    File:
    C:\wamp\www\Big-Sticky-Notes\vendor\zendframework\zendframework\library\Zend\Db\Adapter\Driver\Pdo\Connection.php:286
    Message:
    Connect Error: SQLSTATE[HY000] [1044] Access denied for user ''@'localhost' to database 'stickynotes'
    
    Previous exceptions:
    PDOException
    File:
    C:\wamp\www\Big-Sticky-Notes\vendor\zendframework\zendframework\library\Zend\Db\Adapter\Driver\Pdo\Connection.php:278
    Message:
    SQLSTATE[HY000] [1044] Access denied for user ''@'localhost' to database 'stickynotes'
    
  16. Very nice! Actually I just switched to zend framework and was looking for ways to implement ajax in it after understanding the basics of the zend frameworks’ user guide. It really helped me a lot. Thank you so much for this demo. I got a chance to broaden my mind through this. Eagerly waiting to learn more of ZEND.

  17. I found a potential bug in custom.js line 25;

    $.post("/stickynotes/remove", {
    should be;

    $.post("stickynotes/remove")",{
    You must have corrected this issue in your online demo. With the slash you cannot close the sticky’s you create because its pointed to the wrong location.
    Anyways thanks a lot for the demo very helpful towards my understanding of ZendFramework2

  18. Love the tutorial. The explanations help and it seems much more straightforward than the one in the official docs.

    Since I love it so much, I want to make it as good as it can be. I could help but notice a few typos:

    now Zend Framework -> Now Zend Framework
    that allows to change -> that allows us to change
    Lets create -> Let’s create
    /config/autoloa/local.php -> /config/autoload/local.php
    next lets -> Next, let’s
    trough -> through
    romoveAction -> removeAction

    Minor stuff that doesn’t affect the content; just a bit of the readability. HTH.

    -Brad

  19. Wow Zend Framework 2 is really powerful, I worked on version 1 and was never 100% satisfied, I recently started learning ZF2, and what can I say, I’m really happy with the new architecture, patterns and huge load improvements.

    If we can now just get more guys like yourself to tutor and mentor the developer community about ZF2 – thanks!

  20. For those having problems with the sql setup, try changing this:

    INSERT INTO `stickynotes`.`stickynotes` (`id`, `note`, `created`) VALUES (NULL, 'Let's see if it will work with my iPhone', NULL);

    to this:

    INSERT INTO `stickynotes`.`stickynotes` (`id`, `note`, `created`) VALUES (NULL, "Let's see if it will work with my iPhone", NULL);
  21. Hi There, Stumbled upon this and will give it a go! Do you have any other great tutorials one could learn further on how the ZF2 works? I just got done learning PHP & OOP and looking to dive into some ZF2 apps!

  22. Why this throw this notice?

    Notice: Undefined variable: stickynotes in /var/www/MycreationZF2/module/Application/view/application/index/index.phtml on line 2 Warning: Invalid argument supplied for foreach() in /var/www/MycreationZF2/module/Application/view/application/index/index.phtml on line 2
  23. Very good tutorial, detailed and a clean explanation of crud with Ajax using ZF2, but technically is very ineffective with small up to *medium traffic web applications.
    l will detail this in the following lines, because I encountered this for the first time at under 20 words notes in console log “CANNOT UPDATE” now I began to take the issue very seriously, and my typing is normally only about 45 words / min, a serious problem in my todo website what will deals with transcribe audio and subtitle writing.
    With this method javascript-side overload my local server extremly, that means on each keyup event the Ajax will send a post header to the server requested page and the stickynotes model will update table in the database.
    This is not the big problem, the real problem is when the textarea size increase and the data send over ajax post become larger and larger and larger at every keyboard key released, this means that the update-jscript sends data according to aritmetic progression formula, after 800 characters (included aproximative default post packet size is 650 bytes each [Captured on my local with WShark]), writed on a sticky note on each keypress post will send torrents of data in range (0.85-0.95Mb/keyup), and this traffic is only from 1 user what about 100-200 or more?
    I will try to solve this problem editing custom.js.
    TODO:
    – Creating a global word counter, and then post?

    if(wordsNow != wordsBefore){$.post("stickynotes/update", {..}, 'json');}

    but what about:
    H e l l o w o r l…….
    This sentence will be updated near same as normal keyup, I think is a good choice of a timmer
    to update contents every 2000ms.
    I think the solution is word counter + timer.

    Thank you for this GREAT ARTICLE
    Zend rocks!

  24. I am From Brazil … Excellent Job my friend… But i have one problem with View… Can you Help me ? I rename in everyplace Sticky-notes for CrudBasico ( Basic Crud in Portuguese)

    Invalid argument supplied for foreach() in /Applications/MAMP/bin/mamp/ProjectTeste/module/CrudBasico/view/crud-basico/crud-basico/index.phtml on line 6
  25. this was by far the best tutorial i found for zend 2. the explanation was very simple and to the point. Loved it

  26. Hi, Im really struggling to install Zend , can you provide me with a detail instruction on how to install it in windows 8 ?

    Thanks!
    Amo

  27. Hey 🙂
    I tried your tutorial, but only got a blank page. When I tried the source code from github, I also get a blank page only. Yes, I followed the instructions given, I made a DB with your query and added a local.php with the credentials as well as make a vHost. Any idea whats wrong?

    1. Were you able to install ZF2 correctly at any point? looks like this.

      Were you able to run the composer.php commands correctly? Please refer to ZF2 Packges and composer to configure the composer.json file correctly for the lastest zf2 version. If you already ran the composer commands or have a composer.lock file try removing that file and running composer.phar install

  28. $data = array(
                'username'  =>$object->getUsername(),
                'name'      =>$object->getName(),
                'surname'   =>$object->getSurname(),
                'email'     =>$object->getEmail(),
                'grup_id'   =>$object->getGrup_id(),
                'state'     =>$object->getState(),
            );

    I show username,name,surname,email on form grup_id and state don’t shown on form.

    $entity = new User($form->getData());
    $this->getTable()->save($entity);

    When i use this functions , grup_id and state update null. How can i set only not null value

  29. After I run, it show like this, can you help me?

    Catchable fatal error: Argument 1 passed to StickyNotes\Model\StickyNotesTable::StickyNotes\Model\{closure}() must be an instance of StickyNotes\Model\Select, instance of Zend\Db\Sql\Select given, called in C:\xampp\htdocs\Z2CRUD\vendor\ZF2\library\Zend\Db\TableGateway\AbstractTableGateway.php on line 190 and defined in C:\xampp\htdocs\Z2CRUD\module\StickyNotes\src\StickyNotes\Model\StickyNotesTable.php on line 16
  30. Hello drian,

    I am very new to PHP and Zend 2 and I tried your tutorial and received this error =>
    Zend Framework 2.2.1 application
    Usage:

    Reason for failure: Invalid arguments or no arguments provided

    I was able to see the opening page with the two sticky notes and could edit but nothing is written to the database and I can’t add a new sticky note. I could only assume that my overall configuration is incorrect. Could you kindly help?

    Thanks,

    -Sal

  31. Hello Arian,

    Sorry that I butchered your name in my previous post.

    I accidentally used CLI under Eclipse that generated what I thought was the error. Never mind.

    -Sal

  32. Hello Sir,
    I have a question about zf2 application deployment.
    How to deploy Zend Framework-2 application on shared host of linux server.

    Thanks in advance.

  33. So far, the most excellent tutorial on ZF2. Thank you.

    However, When I click the plus “+” sign, I see no change on the page instead I see the following lines in the Apache log

    File does not exist:/var/www/html/Big-Sticks-Notes-master/public/stickynotes,referer:http://stickynotes.local/

    Thank you

  34. Hi, Arian,

    finished tutorial,
    fixed some mentioned bugs,
    but still, I can only see and edit notes –

    application does not allow adding or deleting these stickies 🙁

    Must be something wrong in js code ?

  35. .. well fixed this by adding space between "remove-"+data.new_note_id and " class="
    custom.js line 8:
    was on tutorial:

       $stickynote.before("<a href="#" rel="nofollow">X</a>");

    corrected to:

       $stickynote.before("<a href="#" rel="nofollow">X</a>");
  36. Good Tutorial my friend

    I am a beginner. Struck over here. Help me out

    Zend\Db\Adapter\Exception\RuntimeException File:
    E:\xampp\htdocs\zend\vendor\zendframework\zendframework\library\Zend\Db\Adapter\Driver\Pdo\Connection.php:294
    Message:
    Connect Error: SQLSTATE[HY000] [1045] Access denied for user ''@'localhost' (using password: NO)
  37. Hi Dear ,

    It’s a fantastic tutorial ,I Ever seen in Zend Framework . We people wait , such good stuffs
    from you .

    All the best

    Thanks,
    Anes

  38. Very good your guide, I’m learning ZF2 and I have a question: How I do to create one module inside other; for example: exist the module name Tools and inside I want create other colling Office.
    How will be the configurationof the files of configuration and how I call through of url…
    because to call someting inside of Tools will be http://zf2/tools/index/index but if I create other module How I do this.
    Greeting

  39. what is the point of using return $this; in every setter method of module/StickyNotes/src/StickyNotes/Model/Entity/StickyNote.php, I think it’s pointless.

  40. Hey, dude.

    Greate post and code. I know this is a post about ZF2 mostly but I think you can change the event to update, on JS file. Just a detail, but it may reduce the requests to server. Changing the event ‘keyup’ to ‘change’, the request will be called only when the content of the textarea is updated.

    1. Totally agree, there are more possible optimization points, but as you mentioned too, this is a post about ZF2 mostly.

      I hope you enjoyed reading it as much as we enjoyed writing it.

      – Best

  41. Thanks your post, it’s very useful.
    I want to copy content in file index.phtml to another file (example view.phtml), but i can’t call ajax in file view.phtml.

  42. In tutorial you create module/StickyNotes/src/StickyNotes/Model/Entity/StickyNote.php yet in your directory structure there is no mention of the Entity directory?

    PS: Good tutorial. I still don’t have it functional, but I can at least follow your instructions unlike many sites. Thank you for sharing!

  43. nice tutorial
    but I am facing a problem on adding new note. when I click on + button nothing happens and give an error “500 internal server error”.
    while delete and update is working fine

  44. Hey,
    Very nice tutorial, I have one query, Is there a way to use a custom controller and extend that in all the other application controller like instead of using AbstractActionController we can create our own class AppController and extending that in all other application controllers.

    Thanks,

  45. There’s an unescaped apostrophe breaking your sql statement. Update it as follows:
    INSERT INTO `stickynotes`.`stickynotes` (`id`, `note`, `created`) VALUES (NULL, ‘Let\’s see if it will work with my iPhone’, NULL)

  46. Hey dude………

    Thanx for this post.
    this article is great. thanks once again….
    i have problem with image upload can you give one demo same about?

  47. This tutorial gives error “The requested controller could not be mapped to an existing controller class.” with latest zend framework skeleton. But when used code on github works. probably small change is necessary to above tutorial. I will try to fix it and update here if i am successful.

  48. after following the tutorial then trying it…Module.php displays…

    // module/StickyNotes/Module.php namespace StickyNotes; use StickyNotes\Model\StickyNotesTable; 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’; } 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; }, ), ); } }

    what should I do??…please help..

  49. I’ve a question:
    How can I insert a start page (simple description project – homepage) and link StickyNotes application?

  50. Wen tryin to hit this URL
    http://127.0.0.1/stickynotes/add

    I am getting
    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’) ->appendFile($this->basePath() . ‘/js/custom.js’) ?>

    Please help .I followed each and every step you suggested.It will be a great help .Thanks in advance.

Leave a Reply

Your email address will not be published. Required fields are marked *