zf2-dpc-tutorial-slides



zf2-dpc-tutorial-slides

7 13


zf2-dpc-tutorial-slides

:books: Slides for the "ZF2: From noob to pro" tutorial for @dpcon

On Github Ocramius / zf2-dpc-tutorial-slides

Unleash the Power of ZF2

Dutch PHP Conference 2014

Marco Pivetta

  • @ocramius
  • Crazy Proxy Guy!

Gary Hockin

  • @GeeH
  • Angry Twitter Guy!

What?

Morning*

  • Introduction (this)
  • Installation
  • Event Manager
  • Service Manager
  • Module Manager
  • Routing

Afternoon*

  • Views
  • Forms
  • Database
  • BONUS! Translations
  • BONUS! Command Line
* Subject to change when timings go horribly awry

Pair Up!

© Rob Allen - All Rights Reserved

Installing

if $wifi:

curl -s https://getcomposer.org/installer | php --
php composer.phar create-project -sdev zendframework/skeleton-application .

else:

$pendrive;

Setup your vhost or (php 5.4+):

php -S 127.0.0.1:80 path/to/install/public

\o/

And so comes some theory...

sorry

Zend\EventManager

The EventManager component is a mix of an observer and a pubsub system

The CCC Problem

Cross-Cutting Concerns

Explain what CCC is about and why it is bad for applications.

A simple user class

class UserService
{
    // ...
    public function logIn(User $user)
    {
        $this->authService->setIdentity($user);
    }
}
A simple user registration class

We also need to log authentication...

class UserService
{
    // ...
    public function logIn(User $user)
    {
        $this->authService->setIdentity($user);

        $this->logger->log('User ' . $user->getId() . ' logged in');
    }
}

... and we get more feature requests over time

public function logIn(User $user)
{
    $this->authService->setIdentity($user);

    $this->logger->log('User ' . $user->getId() . ' logged in');

    if ($this->userDb->getLastLogin($user) < new DateTime('-1 week')) {
        $this->mailer->sendNews($user);
    }

    $this->userDb->incrementActiveUsers();
    $this->flashMessenger->addMessage('Welcome, ' . $user->getName());
    $this->notificationService->notifyFriendsOfLogin($user);
}

Let's simplify it!

class LoginLoggerListener
{
    // ...
    public function logLogin($event)
    {
        $user = $event->getParam('user');

        $this->logger->log('User ' . $user->getId() . ' logged in');
    }
}
class LoginNewsMailerListener
{
    // ...
    public function sendNewsOnLogin($event)
    {
        $user = $event->getParam('user');

        if ($this->userDb->getLastLogin($user) < new DateTime('-1 week')) {
            $this->mailer->sendNews($user);
        }
    }
}

And so on...

Introducing the event manager...

public function logIn(User $user)
{
    $this->authService->setIdentity($user);

    $this->eventManager->trigger('login', $this, ['user' => $user]);
}

... and wiring it together

$eventManager = new Zend\EventManager\EventManager();

$loggerListener     = new LoginLoggerListener(/* ... */);
$newsMailerListener = new LoginNewsMailerListener(/* ... */);

$eventManager->attach('login', [$loggerListener, 'logLogin']);
$eventManager->attach('login', [$newsMailerListener, 'sendNewsOnLogin']);

$userService = new UserService(/* ... */, $eventManager);

Et voilà!

interface EventManagerInterface
{
    /** @return ResponseCollection */
    public function trigger(
        $event,
        $target = null,
        $args = [],
        $callback = null
    );

    public function attach($listener, $callback = null, $priority = 1);

    public function setIdentifiers($identifiers);

    // more stuff ...
}
interface EventInterface
{
    /** @return string */
    public function getName();

    /** @return mixed */
    public function getTarget();

    /** @return array */
    public function getParams();

    public function stopPropagation($flag = true);

    public function propagationIsStopped();

    // more stuff ...
}
interface SharedEventManager
{
    public function attach($identifier, $event, $callback, $priority = 1);

    // more stuff ...
}

Listeners are triggered in the order they are registered if no priority is set:

$eventManager = new EventManager();

$eventManager->attach('event-name', function () {
    echo 'Hello ';
});

$eventManager->attach('event-name', function () {
    echo 'World! ';
});

$eventManager->trigger('event-name');

Listeners are triggered by priority

$eventManager = new EventManager();

$eventManager->attach('event-name', function () {
    echo 'Hello ';
}, -100);

$eventManager->attach('event-name', function () {
    echo 'World! ';
}, 100);

$eventManager->trigger('event-name');

Higher priority = before

Default priority = 1

Event propagation can be stopped:

$eventManager = new EventManager();

$eventManager->attach('event-name', function ($event) {
    echo 'Hello ';

    $event->stopPropagation(true);
});

$eventManager->attach('event-name', function () {
    echo 'World! ';
});

$eventManager->trigger('event-name');

Event propagation can be stopped also from a callback on the trigger side:

$eventManager = new EventManager();

$eventManager->attach('event-name', function () {
    return 'Hello ';
});

$eventManager->attach('event-name', function () {
    return 'World! ' . PHP_EOL;
});

// just run it as usual
$eventManager->trigger('event-name', null, [], function ($result) {
    echo $result;
});

// stop after the first listener
$eventManager->trigger('event-name', null, [], function ($result) {
    echo $result;

    return true;
});

Also called short-circuiting

More resources:

The full UserService

Understanding the EventManager

Publish-Subscribe Pattern

Observer Pattern

Where is Zend\EventManager used in Zend\Mvc?

  • Events are triggered during bootstrap procedures (to allow customization)
  • Routing is just a listener, an event is triggered to allow changing the outcome or catching errors
  • Controller execution is just a part of a "dispatch" listener
  • The entire view layer can be skipped just by stopping propagation of an event
  • And so on... there are a lot!

Where is Zend\EventManager used in Zend\Mvc?

Pretty much everywhere where we may want to apply CCC!

Zend\ServiceManager

The ServiceManager is a Dependency Injection Container and Service Locator

Handling Dependencies

3 levels of "maturity"

Direct instantiation Service Location Dependency Injection Just explain these 3 approaches to fetching dependencies briefly.

Direct instantiation:

class ElePHPant {
    private $drunk = 0;

    public function drinkUntilDrunk()
    {
        $beerTruck = new BeerTruck();

        while ($this->drunk < 1) {
            $beer = $beerTruck->deliverBeer();

            $this->drunk += $beer->getLiters() * $beer->getGrade();
        }

        return 'HIC!';
    }
}

Service Location:

class ElePHPant {
    private $drunk = 0;

    public function drinkUntilDrunk()
    {
        $truck = Registry::get('BeerTruck');

        while ($this->drunk < 1) {
            $beer = $beerTruck->deliverBeer();

            $this->drunk += $beer->getLiters() * $beer->getGrade();
        }

        return 'HIC!';
    }
}

Dependency Injection:

class ElePHPant {
    private $truck;
    private $drunk = 0;
    public function __construct(BeerTruck $truck) {
        $this->truck = $truck;
    }

    public function drinkUntilDrunk()
    {
        while ($this->drunk < 1) {
            $beer = $this->truck->deliverBeer();

            $this->drunk += $beer->getLiters() * $beer->getGrade();
        }

        return 'HIC!';
    }
}

So why Dependency Injection?

  • You get a truck, you don't need to go grab it
  • Your elephpant gets drunken anyway
  • You can change the beer type being delivered

Ok, seriously...

  • Dependencies are given
  • Dependencies are explicit
  • Easier to test
  • Easier to identify bugs and breakages
  • Easier to maintain

When to use Service Location?

Usually, never...

... or when you want get it shipped "quick and dirty" first

Service Location has edge use cases, but in general, it's technical debt

Configuring the Service Manager programmatically:

$serviceManager = new Zend\ServiceManager\ServiceManager();

$serviceManager->setService(...);
$serviceManager->setInvokableClass(...);
$serviceManager->setFactory(...);
$serviceManager->addAbstractFactory(...);
$serviceManager->addInitializer(...);
$serviceManager->addDelegator(...);
$serviceManager->setShared(...);

Configuring the Service Manager with arrays:

$config = [
    'services' => [...],
    'invokables' => [...],
    'factories' => [...],
    'abstract_factories' => [...],
    'initializers' => [...],
    'delegators' => [...],
    'shared' => [...],
];

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config($config);
);

Fetching a service:

$service = $serviceManager->get('service-name');

Service names are Normalized

$service = $serviceManager->get('service-name');

Is the same as:

$service = $serviceManager->get('Service\\Name');

And the same as:

$service = $serviceManager->get('servicename');

Please don't rely on this, it was arguably a bad idea in first place, and it will be removed :-(

Services are Shared by default

$serviceManager->get('service-name') === $serviceManager->get('service-name');

Service Types in the ServiceManager

Basic Services

$truck = new GuinnessTruck();

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'services' => [
            'BeerTruck' => $truck,
        ],
    ])
);

var_dump($truck === $serviceManager->get('BeerTruck'));

Invokable Services

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'invokables' => [
            'BeerTruck' => 'GuinnessTruck,
        ],
    ])
);

var_dump($serviceManager->get('BeerTruck'));

Factories

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'invokables' => [
            'BreweryBrand' => 'Guinness',
        ],
        'factories' => [
            'BeerTruck' => function ($serviceManager) {
                return new BeerTruck($serviceManager->get('BreweryBrand'));
            },
        ],
    ])
);

var_dump($serviceManager->get('BeerTruck'));

Suited for anything that can't be instantiated directly

A factory can either be a callable or the class name of a Zend\ServiceManager\FactoryInterface

Abstract Factories

class MyAbstractFactory implements AbstractFactoryInterface
{
    public function canCreateServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        return fnmatch('*Truck', $requestedName);
    }

    public function createServiceWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName)
    {
        return new $requestedName; // overly simplified!
    }
}
$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'abstract_factories' => ['MyAbstractFactory'],
    ])
);

var_dump($serviceManager->get('BeerTruck'));

Useful for mapping multiple services in one shot!

Shared Services

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'invokables' => [
            'BeerTruck' => 'SimpleBeerTruck',
        ],
        'shared' => [
            'BeerTruck' => false,
        ],
    ])
);

$truck1 = $serviceManager->get('BeerTruck');
$truck2 = $serviceManager->get('BeerTruck');

var_dump($truck1 !== $truck2);

Allows you to use the ServiceManager as a factory

Initializers

$serviceManager = new Zend\ServiceManager\ServiceManager(
    new Zend\ServiceManager\Config([
        'invokables' => [
            'BeerTruck' => 'SimpleBeerTruck',
        ],
        'initializers' => [
            function ($object) {
                if ($object instanceof SimpleBeerTruck) {
                    $truck->setPaint('blue');
                }
            },
        ],
    ])
);

var_dump($serviceManager->get('BeerTruck')->getPaint());

Allows you to apply initialization logic to every object PRODUCED by the ServiceManager

Use carefully!

Delegators

Like initializers, but more flexible and targeted to single services

My advice: use initializers and delegators only when you know the ServiceManager well enough.

Where is Zend\ServiceManager used in Zend\Mvc?

  • To setup the initial application
  • To build/locate View Helpers
  • To build/locate Controller Helpers
  • To create form elements
  • To create routes
  • To build your controllers
  • And so on...

Where is Zend\ServiceManager used in Zend\Mvc?

Wherever you need to "get something by name".

The name may just be in your code, or in a configuration file/format

Zend\ModuleManager

Re-usability === GOOD

GOOD as in BEER

© Ben Scholzen - All Rights Reserved

Module Manager

Allows re-using of code including

  • Routes
  • Controllers
  • Views (Layouts)
  • Services
  • Modules
  • Helpers (Controller, View, etc)

Basically everything.

Module Manager

  • Modules typically are stored in module folder
  • Typically have a src folder that is PSR compliant
  • Modules have a Module.php that tells the MM how to work with them
  • Modules present configuration that is merged into the global "merged config"
  • Modules are awesome.

Module.php

namespace MyModule;

class Module
{
    public function getConfig() {}

    public function getControllerConfig() {}

    public function getServiceConfig() {}

    public function getAutoloaderConfig() {}

    public function onBootstrap() {}

    public function init() {}
}

Typical Config

return [
    'router' => [
        'routes' => [
            ...
        ],
    ],
    'service_manager' = [
        'factories' => [
            ...
        ],
    ],
    'controllers' => [
    ...
];

Configs Provided By

Arrays returned from a module's getConfig method (typically config/module.config.php) config/autoload/global.*.php files config/autoload/local.*.php files The module's ServiceManager configs - get*Config

1-3 can be cached by the Module Manager easily

application.config.php

Tells the whole application how to behave and which modules to load

return [
    'modules' => [
        'Application',
        'ZfcBase',
        'ZfcUser',
    ],
    'module_listener_options' => [
        'module_paths' => [
            './module',
            './vendor',
        ],
        'config_glob_paths' => [
            'config/autoload/{,*.}{global,local}.php',
        ],
    ],
    ...
];

Zend\Mvc\Router

Zend\Mvc\Router

Translates the URI from the HTTP request into a class and method that should process it (typically a controller and action)

/hello/world => My\Controller\Hello::worldAction()

Zend\Router

Configured by any router key in the merged config

[
    'router' => [
        'routes' => [
            'home' => [
                'type' => 'Zend\Mvc\Router\Http\Literal',
                'options' => [
                    'route'    => '/',
                    'defaults' => [
                        'controller' => 'Application\Controller\Index',
                        'action'     => 'index',
                    ]
                ]
            ]
        ]
    ]
]
/ => Application\Controller\Index::indexAction()

Controller Manager

To add to the confusion...

Application\Controller\Index is a key in the ControllerManager service locator.

function getControllerConfig() {
    return [
        'invokables' => [
            'Application\Controller\Index' => 'Application\Controller\IndexController'
        ]
    ];
}

Route Types

There are different types of routes:

Zend\Mvc\Router\Http\Literal

Matches to the exact string of the URI path

'route' => '/ghostbuster/egon'
  • /ghostbuster/egon will match
  • /ghostbuster/egon/ will NOT match

(note lack of trailing slash)

Zend\Mvc\Router\Http\Segment

Matches any segment of the URI path.

Parameters are donated with a colon and are passed through to the controller

Square brackets donate optional parameters

'route' => '/ghostbuster/:name[/]'
  • /ghostbuster/egon will match
  • /ghostbuster/egon/ will match

name parameter will be set to egon

Zend\Mvc\Router\Http\Regex

Uses a regular expression to match against the URI

Matched parts can be converted to named parameters using a spec

'regex' => '/ghostbuster/(?<name>[a-zA-Z0-9_-]+)(\.(?<type>(field|clerical|friend)))?',
'spec' => '/ghostbuster/%name%.%type%'
  • /ghostbuster/janine.clerical will match
  • /ghostbuster/ray.catcher will NOT match

name parameter will be set to janine

type parameter will be set to clerical

Zend\Mvc\Router\Http\Scheme

Matches only given protocol

'scheme' => 'https'

Zend\Mvc\Router\Http\Method

Matches only given HTTP method verb(s)

'verb' => 'get,post'

SimpleRouteStack

Simply iterates through route definitions in a LIFO order unit it gets a match.

TreeRouteStack

Allows complex definitions of routes in a tree structure. Uses B-tree algorithm to match the routes.

Use may_terminate to tell the router no other routes will follow

Use child_routes to define any children of an existing route

MAKE THIS LOOK BETTER

'base' => [
    'type' => 'literal',
    'options' => [
        'route' => '/ghostbuster',
        'defaults' => [
            'controller' => 'Ghostbuster',
            'action' => 'base',
        ]
    ],
    'may_terminate' => true,
    'child_routes' => [
        'people' => [
            'type' => 'segment',
            'options' => [
                'route' => '/[:name]',
                'constraints' => [
                    'name' => '[a-zA-Z0-9_-]+'
                ],
                'defaults' => [
                    'action' => 'people',
                ],
            ],
        ],
    ],
],
  • /ghostbuster will match base route
  • /ghostbuster/ray will match people route
  • constraints key allows regex matching of parameters

Router View Helpers

url

Composes an uri from route name and parameters

<?= $this->url('base/people', ['name' => 'ray']); ?>

gives...

<a href="/ghostbuster/ray"></a>

Exercise

Add a new Module to your skeleton app called Blog

The module should initially route /blog to a brand new controller and action in that module

30 minutes

Once finished feel free to go to lunch

Hint

  • Create a new folder called "Blog" in your "modules" directory
  • Create a file called Module.php in that directory
  • Create a config directory and a module.config.php
  • You will also need to create the view/blog/blog folder
  • You will need a "Hello World" html file in the index.phtml view script

modules/Blog/Module.php

namespace Blog;

class Module
{

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

    public function getAutoloaderConfig()
    {
        return [
            'Zend\Loader\StandardAutoloader' => ['namespaces' => [
                __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__,
            ]],
        ];
    }


    public function getControllerConfig()
    {
        return [
            'invokables' => [
                'Blog\Controller\Blog' => 'Blog\Controller\BlogController',
            ],
        ];
    }

}
modules/Blog/config/module.config.php
return [
    'router' => [
        'routes' => [
            'blog' => [
                'type' => 'Zend\Mvc\Router\Http\Literal',
                'options' => [
                    'route' => '/blog',
                    'defaults' => [
                        'controller' => 'Blog\Controller\Blog',
                        'action' => 'index',
                    ],
                ],
            ],
        ],
    ],
    'view_manager' => [
        'display_not_found_reason' => true,
        'display_exceptions' => true,
        'doctype' => 'HTML5',
        'template_map' => [],
        'template_path_stack' => [
            __DIR__ . '/../view',
        ],
    ],
];

Zend\Db

Zend\Db

A Database Abstraction Layer that allows easy and safe connection to many database platforms, includes:

  • Platform
  • Adapter
  • TableGateway
  • Sql Generation

Zend\Db\Adapter

Provides a connection to the database platform, usually via configuration through the merged config

config/db.local.config.php
return [
    'db' => [
        'driver' => 'Pdo',
        'dsn' => 'mysql:mydb=blog;host=127.0.01',
        'username' => 'keymaster',
        'password' => 'gatekeeper',
    ],
    'service_manager' => [
        'factories' => [
            'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
        ],
    ],
];

DO NOT STORE IN YOUR VERSION CONTROL!!!

Zend\Db\Adapter

We're safely connected!

(We do not recommend the crossing of streams)

Zend\Db\TableGateway

Allows pain-free view into a single table including all the usual suspects:

  • Select
  • Update
  • Delete

Zend\Db\TableGateway

Creating:

return [
    'factories' => [
        'GhostTableGateway' => function (ServiceManager $serviceManager) {
            $adapter = $serviceManager->get('Zend\Db\Adapter\Adapter');
            $tableGateway = new TableGateway('ghost', $adapter);
        }
    ],
];

Zend\Db\TableGateway

Find all the clerical staff:

$clericalStaff = $tableGateway->select(['type' => 'clerical']);

Fire all the clerical staff:

$tableGateway->delete(['type' => 'clerical']);

Give all the Ghostbusters a capture:

$tableGateway->update(
    ['captures' => new Expression('captures + 1')],
    ['type' => 'ghostbuster']
);

Zend\Db\TableGateway

Using:

'factories' => [
    'Ghostbuster\Controller\Ghost' => function(ControllerManager $cm) {
        $ghostTable = $cm->getServiceLocator()->get('GhostTable');
        return new GhostController($ghostTable);
    },
],
GhostController
public function __construct(TableGateway $ghostTable)
{
    $this->ghostTable = $ghostTable;
}

public function viewAction()
{
    $this->ghostTable->select(['id' => 666]);
}

DON'T DO THIS!!!

Zend\Db\TableGateway

Use a Service (Mapper) Layer:

Controller Config
'GhostService' => function(ServiceManager $sm) {
    $ghostTable = $sm->get('GhostTableGateway');
    return new GhostService($postTable);
},
Service Config
'Ghostbuster\Controller\Ghost' => function(ControllerManager $cm) {
    $ghostService = $cm->getServiceLocator()->get('GhostService');
    return new GhostController($ghostService);
},
GhostController
public function __construct(GhostService $ghostService)
{
    $this->ghostService = $ghostService;
}

public function viewAction()
{
    $this->ghostService->findGhostById(666);
}

Why use an intermediary?

  • Pre-processing
  • Caching
  • Changes

Hydration

The act of turning an array into an object (and vice-versa)...

because it's much easier to pass around objects than arrays.

$hydrator = new \Zend\Stdlib\Hydrator\ClassMethods();
$data = ['name' => 'Egon', 'type' => 'Ghostbuster', 'captures' => 39];
/** @var Ghostbuster $ghostbuster */
$ghostbuster = $hydrator->hydrate($data, new Ghostbuster());

Entity:

class Ghostbuster
{
    protected $name;
    protected $type;
    protected $captures;

    public function setCaptures($captures)
    {
        $this->captures = $captures;
    }
    public function getCaptures()
    {
        return $this->captures;
    }
    ...

Hydrating Results Sets

Get automagically hydrated objects back from your DbTable queries

MEGA WIN!!!

Hydrating Results Sets

Just update your TableGateway definition in Service Manager:

'PostTableGateway' => function (ServiceManager $serviceManager) {
    $adapter = $serviceManager->get('Zend\Db\Adapter\Adapter');
    $hydrator = new ClassMethods(true);
    $rowObjectPrototype = new Ghostbuster(); // Entity to hydrate
    $resultSet = new HydratingResultSet($hydrator, $rowObjectPrototype);
    $tableGateway = new TableGateway('people', $adapter, null, $resultSet);
    return $tableGateway;
}

Exercise

  • Create a new MySQL database and add the blog table (schema is available)
  • Create a new Post entity that describes the post table
  • Create a new Adapter, PostService and TableGateway for the blog table
  • The TableGateway should hydrate the Post entity
  • Make sure the BlogController can access the PostService

30 minutes

Once finished feel free to go to break

Hint

  • Add a new db key to the merged config via a config/autoload/db.local.php file
  • Create folders in the modules src directory Entity & Service
  • Create a new Post entity in the Entity folder, the entity should have same properties as the table, and have getters and setters
  • Create a new PostService class that takes the BlogTable as a dependency (including SM definition)
  • Create a new BlogTable TableGateway - this will only need to be defined in the SM
  • Update the BlogController to take the PostService as a dependency, and pass it in via the Controller Manager
config/autload/db.local.php
return [
    'db' => [
        'driver' => 'Pdo',
        'dsn' => 'mysql:dbname=blog;host=127.0.01',
        'driver_options' => [
            PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
        ],
        'username' => 'blog',
        'password' => 'rayparkerjr',
    ],
    'service_manager' => [
        'factories' => [
            'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
        ],
    ],
];
module/Blog/src/Blog/Entity/Post.php
namespace Blog\Entity;

class Post
{
    protected $id;
    protected $slug;
    protected $writtenOn;
    protected $title;
    protected $preview;
    protected $views;
    protected $body;

    /**
     * @return mixed
     */
    public function getBody()
    {
        return $this->body;
    }

    /**
     * @param mixed $body
     */
    public function setBody($body)
    {
        $this->body = $body;
    }

    /**
     * @return mixed
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * @param mixed $id
     */
    public function setId($id)
    {
        $this->id = $id;
    }

    /**
     * @return mixed
     */
    public function getPreview()
    {
        return $this->preview;
    }

    /**
     * @param mixed $preview
     */
    public function setPreview($preview)
    {
        $this->preview = $preview;
    }

    /**
     * @return mixed
     */
    public function getSlug()
    {
        return $this->slug;
    }

    /**
     * @param mixed $slug
     */
    public function setSlug($slug)
    {
        $this->slug = $slug;
    }

    /**
     * @return mixed
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * @param mixed $title
     */
    public function setTitle($title)
    {
        $this->title = $title;
    }

    /**
     * @return mixed
     */
    public function getViews()
    {
        return $this->views;
    }

    /**
     * @param mixed $views
     */
    public function setViews($views)
    {
        $this->views = $views;
    }

    /**
     * @return mixed
     */
    public function getWrittenOn()
    {
        return $this->writtenOn;
    }

    /**
     * @param mixed $writtenOn
     */
    public function setWrittenOn($writtenOn)
    {
        $this->writtenOn = $writtenOn;
    }

}
module/Blog/src/Blog/Service/PostService.php
namespace Blog\Service;

use Zend\Db\TableGateway\TableGateway;


class PostService
{

    /**
    * @var TableGateway
    */
    protected $postTable;

    function __construct(TableGateway $postTable)
    {
        $this->postTable = $postTable;
    }
}
module/Blog/src/Blog/Service/PostService.php
namespace Blog\Service;

use Zend\Db\TableGateway\TableGateway;


class PostService
{

    /**
    * @var TableGateway
    */
    protected $postTable;

    function __construct(TableGateway $postTable)
    {
        $this->postTable = $postTable;
    }
}
module/Blog/Module.php
public function getServiceConfig()
{
    return [
        'factories' => [
            'Blog\Service\PostService' => function(ServiceManager $serviceManager) {
                $postTable = $serviceManager->get('PostTableGateway');
                return new PostService($postTable);
            }
        ],
    ];
}
module/Blog/Module.php
public function getServiceConfig()
{
    return [
        'factories' => [
            'PostTableGateway' => function (ServiceManager $serviceManager) {
                $adapter = $serviceManager->get('Zend\Db\Adapter\Adapter');
                $hydrator           = new ClassMethods(true);
                $rowObjectPrototype = new Post();
                $resultSet          = new HydratingResultSet($hydrator, $rowObjectPrototype);
                $tableGateway       = new TableGateway('post', $adapter, null, $resultSet);
                return $tableGateway;
            },
            'Blog\Service\PostService' => function(ServiceManager $serviceManager) {
                $postTable = $serviceManager->get('PostTableGateway');
                return new PostService($postTable);
            }
        ],
    ];
}
module/Blog/Module.php
public function getControllerConfig()
{
    return [
        'factories' => [
            'Blog\Controller\Blog' => function(ControllerManager $controllerManager) {
            $postService = $controllerManager->getServiceLocator()->get('Blog\Service\PostService');
            return new BlogController($postService);
        },
    ];
}
module/Blog/src/Controller/BlogController.php
namespace Blog\Controller;

use Blog\Service\PostService;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

class BlogController extends AbstractActionController
{
    /**
    * @var PostService
    */
    protected $postService;

    function __construct(PostService $postService)
    {
        $this->postService = $postService;
    }
}

Zend\View

Deals with rendering view scripts (PHP/Twig/Markdown/etc) into whatever format you want

View Script

A view script is just a file.

In Zend\Mvc, view scripts are .phtml files.

View scripts are usually in your modules, in a directory called view/

View Script Resolution

Zend\Mvc won't look for your view scripts automagically.

View script paths are "resolved" by a Zend\View\Resolver\ResolverInterface.

For simplicity, we will just see how to configure the view layer for our needs.

View Manager Configuration

In order to get Zend\Mvc to find our scripts, we have to tell it where to find them in our config/module.config.php:

'view_manager' => array(
    'template_path_stack' => array(
        __DIR__ . '/../view',
    ),
],

View Paths

By default, view names are built from the dispatched controller name:

<root-namespace>/<controller-class-name>/<action>

So if Blog\Controller\BlogController#postAction() is executed the view name will be ...

blog/blog/post

... which in our case translates to view script ...

module/Blog/view/blog/blog/post.phtml

Creating a view script

Create a file module/Blog/view/blog/blog/post.phtml and put following contents in it

<?php

echo "Hello World!";

And that's it! Just normal PHP, nothing to see here

View Script Variables

A view script is "filled" with variables from the ViewModel that is returned by the controller.

public function indexAction()
{
    return new ViewModel(['foo' => 'bar']);
}

<?php
echo $foo;
echo $this->foo;

Both approaches work!

View Helpers

View helpers are utility methods that are available in view scripts:

<?php

<a href="<?= $this->escapeHtmlAttr($userDefinedUrl); ?>">
    <?= $this->escapeHtml($userDefinedText); ?>
</a>

In this example escapeHtml and escapeHtmlAttr are helpers.

Where are View Helpers defined?

View helpers are defined in a specific ServiceManager (PluginManager) that is pre-configured.

See the HelperPluginManager for a list of pre-defined helpers.

You can register your own helpers: it's just about configuring a specific service manager.

Exercise: List all BlogPosts

Write a view that does:

  • Render a list of all blog-posts
  • Render (and escape) every blog-post title (use a helper)
  • Render the date when the blogpost was written
  • Render a link to a detail view of the blogpost (use the URL helper and the blog-post slug)

Exercise: List all BlogPosts

in

module/Blog/view/blog/blog/index.phtml

<?php foreach ($posts as $post): ?>
    <hr/>
    <a href="<?php echo $this->url('post', ['slug' => $post->getSlug()]); ?>">
        <h1><?php echo $this->escapeHtml($post->getTitle()); ?></h1>
    </a>
    <h3><?php echo $this->escapeHtml($post->getWrittenOn()); ?></h3>
    <p><?php echo $this->escapeHtml($post->getPreview()); ?></p>
<?php endforeach; ?>

Zend\Form

Why?

Forms are hard work, who wants to be writing HTML forms and validating input by hand?

Create The Form

use Zend\Form\Form

class MyForm extends Form
{
}

Add Some Elements

public function __construct()
{
    $this->add(
        [
            'name' => 'name', // for added confusion
            'type' => 'text',
            'options' => [
                'label' => 'Name'
            ],
        ],
    );
    $this->add(
        [
            'name' => 'submit',
            'type' => 'submit',
        ]
    );
}

Render in the view!

Controller

return new ViewModel(
    ['form' => new MyForm()]
);

View

echo $this->form()->openTag($form);
echo $this->formCollection($form);
echo $this->form()->closeTag();

Add Some Validation

protected $inputFilter;
...
public function getInputFilter()
{
    if (!$this->inputFilter) {
        $this->inputFilter = new InputFilter();
        $this->inputFilter->add([
            'name' => 'name', // same as name of element
            'required' => 'true',
            'filters' => [['name' => 'StringTrim']],
            'validators' => [[
                'name' => 'StringLength',
                'options' => [
                    'min' => 3,
                    'max' => 64
                ]
            ]],
        ]);
    }
    return $this->inputFilter;
}

Element Types:

Handling:

public function addAction()
{
    $form = new PostForm();
    $ghostbuster = new Ghostbuster();
    $form->setHydrator(new ClassMethods())->setObject($ghostbuster);
    //hydration rocks!
    if ($this->getRequest()->isPost()) { // form has been posted
        $form->setData($this->getRequest()->getPost()->toArray());
        if($form->isValid()) { // form validates so save it!
            $this->ghostbusterService->savePost($form->getObject());
            $this->redirect()->toRoute('view');
        }
    } else {
        $form->bind($ghostbuster);
    }
    return new ViewModel(['form' => $form]);
}

Exercise

  • Create a new action called add and a route that handles it
  • Create a new PostForm (complete with sensible validators)
  • Use the add view to display the form
  • Create a method of the PostService to save the form
  • Redirect to the post list to show the new post in the list

Hint

  • Add a new class at module/Blog/src/Blog/Form/PostForm.php
  • Add the elements to the form using the add method
  • Add the lazy loaded validators/filters to the getInputFilter method
  • Create a new add.phtml and output the form using the view helpers
  • Add the form handler code (should be similar to example!)
module/Blog/src/Blog/Form/PostForm.php
namespace Blog\Form;

use Zend\Form\Form;
use Zend\InputFilter\InputFilter;

class PostForm extends Form
{

    protected $inputFilter;

    public function __construct()
    {
        parent::__construct('post');

        $this->add(
            [
                'name' => 'id',
                'type' => 'hidden'
            ]
        );

        $this->add(
            [
                'name' => 'slug',
                'type' => 'text',
                'options' => [
                    'label' => 'Slug'
                ],
                'attributes' => [
                    'class' => 'form-control'
                ],
            ]
        );

    $this->add(
        [
            'name' => 'written_on',
            'type' => 'text',
            'options' => [
                'label' => 'Written On'
            ],
            'attributes' => [
            '   class' => 'form-control',
                'readonly' => true
            ],
        ]
    );

    $this->add(
        [
            'name' => 'title',
            'type' => 'text',
            'options' => [
                'label' => 'Title'
            ],
            'attributes' => [
                'class' => 'form-control'
            ],
        ]
    );

    $this->add(
        [
            'name' => 'preview',
            'type' => 'textarea',
            'options' => [
                'label' => 'Preview'
            ],
                'attributes' => [
                'class' => 'form-control'
            ],
        ]
    );

    $this->add(
        [
            'name' => 'body',
            'type' => 'textarea',
            'options' => [
                'label' => 'Body'
            ],
            'attributes' => [
                'class' => 'form-control',
                'rows' => 20
            ],
        ]
    );

    $this->add(
        [
            'name' => 'views',
            'type' => 'hidden',
            'value' => '0',
        ]
    );

    $this->add(
        [
            'name' => 'submit',
            'type' => 'submit',
            'attributes' => [
                'class' => 'btn btn-default margin-top'
            ],
        ]
    );
}

public function getInputFilter()
{
    if (!$this->inputFilter) {
        $this->inputFilter = new InputFilter();

        $this->inputFilter->add(
        [
        'name' => 'slug',
        'required' => 'true',
        'filters' => [
        ['name' => 'StringTrim'],
        ],
        'validators' => [
        [
        'name' => 'Regex',
        'options' => [
        'pattern' => '/^[a-z0-9-]+$/'
        ]
        ],
        [
        'name' => 'StringLength',
        'options' => [
        'min' => 3,
        'max' => 64
        ]
        ]
        ]
        ]
        );

        $this->inputFilter->add(
            [
                'name' => 'title',
                'required' => true,
                'filters' => [
                    [
                        'name' => 'StringTrim',
                        'name' => 'StripTags',
                    ]
                ],
                'validators' => [
                    [
                        'name' => 'StringLength',
                        'options' => [
                            'min' => 3,
                            'max' => 64
                        ]
                    ]
                ]
            ]
        );

        $this->inputFilter->add(
            [
                'name' => 'views',
                'required' => false
            ]
        );

        $this->inputFilter->add(
            [
                'name' => 'preview',
                'required' => true,
            ]
        );

        $this->inputFilter->add(
            [
                'name' => 'body',
                'required' => true,
            ]
        );
    }

    return $this->inputFilter;
    }

}
module/Blog/view/blog/add.phtml
<!--?php
/** @var \Blog\Entity\Post $post */
?-->

<h3>Add Post</h3>

<?php
    $form->setAttribute('action', $this->url());
    echo $this->form()->openTag($form);
    echo $this->formCollection($form);
    echo $this->form()->closeTag();
?>
module/Blog/src/Blog/Controller/BlogController.php
public function addAction()
{
    $form = new PostForm();
    $post = new Post();

    $form->setHydrator(new ClassMethods())->setObject($post);

    if ($this->getRequest()->isPost()) {
        $form->setData($this->getRequest()->getPost()->toArray());
        if($form->isValid()) {
            $this->postService->savePost($form->getObject());
            $this->redirect()->toRoute('blog');
        }
    } else {
        $form->bind($post);
    }

    $form->get('submit')->setValue('Add');

    return new ViewModel(['post' => $post, 'form' => $form]);
}