Slide 1

Slide 1 text

Модули в Zend Framework 2 Rostislav Mykhajliw oDesk/TrueSocialMetrics

Slide 2

Slide 2 text

Modules Configuration DI Events Testing Distribution Trick Tips

Slide 3

Slide 3 text

Init github: ZendSkeletonApplication github: ZendSkeletonModule

Slide 4

Slide 4 text

Init ZF2 application ├── README.md ├── composer.json ├── config │ ├── application.config.php │ └── autoload │ └── global.php │ └── local.php ├── data ├── init_autoloader.php ├── module │ └── Application ├── public └── vendor └── ZF2

Slide 5

Slide 5 text

Init ZF2 module ├── Module.php ├── README.md ├── autoload_classmap.php / autoload_function.php / autoload_register.php ├── config │ └── module.config.php ├── src │ └── ZendSkeletonModule ├── tests │ ├── TestConfiguration.php.dist │ ├── ZendSkeletonModule │ ├── bootstrap.php │ └── phpunit.xml └── view └── zend-skeleton-module

Slide 6

Slide 6 text

Configuration

Slide 7

Slide 7 text

Init ZF2 module namespace ZendSkeletonModule; use Zend\ModuleManager\Feature\AutoloaderProviderInterface; use Zend\Mvc\ModuleRouteListener; class Module implements AutoloaderProviderInterface { public function getAutoloaderConfig() public function getConfig() public function onBootstrap($e); }

Slide 8

Slide 8 text

Init ZF2 module - simple. namespace ZendSkeletonModule; class Module { public function getConfig() { echo "I was there"; exit; } } Works!

Slide 9

Slide 9 text

Config :: Why? public function onLoadModule(ModuleEvent $e) { $module = $e->getModule(); if (!$module instanceof ConfigProviderInterface && !is_callable(array($module, 'getConfig')) ) { return $this; } $config = $module->getConfig(); $this->addConfig($e->getModuleName(), $config); return $this; } Zend\ModuleManager\Listener\ConfigListener

Slide 10

Slide 10 text

Config :: interfaces Zend\ModuleManager\Feature ├── AutoloaderProviderInterface => getAutoloaderConfig ├── BootstrapListenerInterface => onBoostrap ├── ConfigProviderInterface => getConfig ├── ConsoleBannerProviderInterface => getConsoleBanner (in console) ├── ConsoleUsageProviderInterface => getConsoleUsage (in console) ├── ControllerPluginProviderInterface => getControllerPluginConfig() ├── ControllerProviderInterface => getControllerConfig() ├── InitProviderInterface => init(ModuleManagerInterface $manager) ├── LocatorRegisteredInterface => LOAD by DI ├── ServiceProviderInterface => getServiceConfig() └── ViewHelperProviderInterface => getViewHelperConfig()

Slide 11

Slide 11 text

Config :: merging application.config.php -> foreach modules configs -> Application module -> Api module -> ... -> global.php -> local.php

Slide 12

Slide 12 text

Config :: merging 'TweeCdn\View\Helper\Cdn\AbstractCdn' => array( 'parameters' => array( 'type' => 'release', 'options' => array( 'hostnames' => array('http://cdn.com'), 'release' => trim(file_get_contents(__DIR__ . '/../../REVISION')), ), ), ), global.php

Slide 13

Slide 13 text

Config :: merging 'TweeCdn\View\Helper\Cdn\AbstractCdn' => array( 'parameters' => array( 'type' => 'release', 'options' => array( 'hostnames' => array(''), 'release' => '000000000', ), ), ), local.php

Slide 14

Slide 14 text

Config :: merging

Slide 15

Slide 15 text

Config :: merging

Slide 16

Slide 16 text

Config :: merging :: Why ? $a = array(1, 2); $b = array(2, 3); var_dump(array_merge($a, $b)); array(4) { [0] => int(1) [1] => int(2) [2] => int(2) [3] => int(3) } use ONLY string KEYS !

Slide 17

Slide 17 text

Config :: recommendation module config: - default base configuration. Minimum config to work (routes, views) global config: - global things, DI definition, production settings local config: - local, environment dependable (db host)

Slide 18

Slide 18 text

DI (Dependency Injection)

Slide 19

Slide 19 text

DI :: configuration return array( 'router' => array( 'routes' => array( 'controllers' => array( 'invokables' => array( 'twitter' => 'Twitter\Controller\IndexController', 'di' => array( 'definition' => array( 'class' => array( ... ) 'instance' => array( 'alias' => array( ... )

Slide 20

Slide 20 text

DI :: configuration

Slide 21

Slide 21 text

DI :: configuration :: application key key = $key; } public function getApplicatonKey() { return $key; } public function indexAction() {} }

Slide 22

Slide 22 text

DI :: configuration :: application key 'di' => array( 'instance' => array( 'Twitter\Controller\IndexController' => array( 'parameters' => array( 'key' => 'my-secret-application-key' ), ), ), ), Let's add options to though DI config Works ??? NO.

Slide 23

Slide 23 text

DI :: Why ? protected function createFromInvokable($canonicalName, $requestedName) { $invokable = $this->invokableClasses[$canonicalName]; $instance = new $invokable(); return $instance; } You have Invokables CONTROLLERS !!! Zend\ServiceManager\AbstractPluginManager

Slide 24

Slide 24 text

DI :: configuration / Works return array( 'router' => array( 'routes' => array( 'controllers' => array( 'invokables' => array( // 'twitter' => 'Twitter\Controller\IndexController', Remove class from invokables and replace to factories!

Slide 25

Slide 25 text

DI :: Factories / Works namespace Twitter; class Module implements Feature\ControllerProviderInterface { public function getControllerConfig() { return array('factories' => array( 'Twitter\Controller\TwitterController' => function ($cm) { $sm = $cm->getServiceLocator(); $config = $sm->get('config'); $key = $config['di']['instance']['TwitterController']['parameters']['key']; $controller = new Controller\IndexController(); $controller->setApplicationKey($key); return $controller; } )); }

Slide 26

Slide 26 text

DI :: factories :: interfaces namespace Application\Controller; interface TokenInterface { public function setToken($token); } namespace Application\Controller; class IndexController implements TokenInterface { private $token = ''; public function setToken($token) { $this->token = $token; } }

Slide 27

Slide 27 text

DI :: factories :: interfaces class Module implements Feature\ControllerProviderInterface public function getControllerConfig() { return array('factories' => array( 'Twitter\Controller\TokenInteface' => function ($cm, $cmKey, $name) { // $cm - Zend\Mvc\Controller\ControllerManager // $cmKey - internal key for service locator // $name - class name $sm = $cm->getServiceLocator(); $config = $sm->get('config'); $token = $config['di']['instance'][$name]['parameters']['token']; $controller = new $name(); $controller->setToken($token); return $controller;

Slide 28

Slide 28 text

DI :: configuration :: factories return array( 'controllers' => array( 'invokables' => array( // 'twitter' => 'Twitter\Controller\IndexController', 'factories' => array( 'Index' => 'Twitter\Controller\Factory\Token' namespace Twitter\Controller\Factory; use \Zend\ServiceManager\FactoryInterface; use \Zend\ServiceManager\ServiceLocatorInterface; class Token implements FactoryInterface { public function createService(ServiceLocatorInterface $serviceLocator) {} }

Slide 29

Slide 29 text

Events

Slide 30

Slide 30 text

Events namespace Zend\Mvc; class MvcEvent extends Event { const EVENT_BOOTSTRAP = 'bootstrap'; const EVENT_DISPATCH = 'dispatch'; const EVENT_DISPATCH_ERROR = 'dispatch.error'; const EVENT_FINISH = 'finish'; const EVENT_RENDER = 'render'; const EVENT_ROUTE = 'route'; }

Slide 31

Slide 31 text

Events :: Loading namespace Zend\ModuleManager; class ModuleEvent extends Event { CONST EVENT_LOAD_MODULES = 'loadModules'; CONST EVENT_LOAD_MODULE_RESOLVE = 'loadModule.resolve'; CONST EVENT_LOAD_MODULE = 'loadModule'; CONST EVENT_LOAD_MODULES_POST = 'loadModules.post'; }

Slide 32

Slide 32 text

Events :: workflow

Slide 33

Slide 33 text

Events :: Bootstrap 'Congratulations!' )); } }

Slide 34

Slide 34 text

Events :: Bootstrap namespace Api; use Zend\ModuleManager\Feature, Zend\EventManager\EventInterface, Zend\Mvc\MvcEvent; class Module implements Feature\BootstrapListenerInterface { public function onBootstrap(EventInterface $e) { $app = $e->getApplication(); $em = $app->getEventManager()->getSharedManager(); $sm = $app->getServiceManager(); $em->attach(__NAMESPACE__, MvcEvent::EVENT_DISPATCH, function($e) use ($sm) { $strategy = $sm->get('ViewJsonStrategy'); $view = $sm->get('ViewManager')->getView(); $strategy->attach($view->getEventManager()); }); }

Slide 35

Slide 35 text

Events :: Bootstrap :: in config return array( 'view_manager' => array( 'strategies' => array( 'ViewJsonStrategy', ), ), ); All modules affected!

Slide 36

Slide 36 text

Controllers

Slide 37

Slide 37 text

AbstractController abstract class AbstractController { public function getEventManager(); // EventManager public function getServiceLocator(); // ServiceManager public function plugin($name, $options=null) public function getRequest(); public function getResponse(); }

Slide 38

Slide 38 text

AbstractActionController class IndexController extends AbstractActionController { public function indexAction() { $name = $this->getRequest() ->getQuery()->get('name'); $book = $this->getServiceLocator() ->get('Repository/Book') ->find($name); return new ViewModel(array( 'book' => $book )); } }

Slide 39

Slide 39 text

AbstractActionController class IndexController extends AbstractActionController { public function pictureAction() { $response = new Response(); $response->setStatusCode(Response::STATUS_CODE_200); $response->getHeaders() ->addHeaderLine('Content-Type: image/png') ->addHeaderLine('Content-Length: ' . (string)filesize($file)); $response->setContent(file_get_ocntents($file)); return $response; } } It ignores everything! and returns response.

Slide 40

Slide 40 text

AbstractRestfulController abstract class AbstractRestfulController extends AbstractController { abstract public function getList(); abstract public function get($id); abstract public function create($data); abstract public function update($id, $data); abstract public function delete($id); }

Slide 41

Slide 41 text

AbstractRestfulController class IndexController extends AbstractRestfulController { public function getList() { $response = $this->getResponse(); $response->setStatusCode(200); $response->setContent(new ModelJson(array( array('name' => 'jo'), array('name' => 'peter'), ))); return $response; } } Sample

Slide 42

Slide 42 text

Rest :: ZF 2.0.4 (broken DI) class SomeController extends AbstractActionController { protected $acceptCriteria = array( 'Zend\View\Model\JsonModel' => array( 'application/json', ), 'Zend\View\Model\FeedModel' => array( 'application/rss+xml', ), ); public function apiAction() { $viewModel = $this->acceptableViewModelSelector($this->acceptCriteria); // Potentially vary execution based on model returned if ($viewModel instanceof JsonModel) { // ... } } }

Slide 43

Slide 43 text

Testing and Distribution

Slide 44

Slide 44 text

Testing :: tests ├── TestConfiguration.php.dist ├── ZendSkeletonModule │ ├── Framework │ │ └── TestCase.php │ └── SampleTest.php ├── bootstrap.php └── phpunit.xml

Slide 45

Slide 45 text

Testing :: Works (but slow) namespace Application\Controller; class IndexControllerTest extends Framework\ControllerTestCase { protected function setUp() { $bootstrap = \Zend\Mvc\Application::init(include 'application.php'); $this->controller = new IndexController(); $this->request = new Request(); $this->routeMatch = new RouteMatch(array('controller' => 'index')); $this->event = $bootstrap->getMvcEvent(); $this->event->setRouteMatch($this->routeMatch); $this->controller->setEvent($this->event); $this->controller->setEventManager($bootstrap->getEventManager()); $this->controller->setServiceLocator($bootstrap->getServiceManager()); } }

Slide 46

Slide 46 text

Testing

Slide 47

Slide 47 text

Tests :: be simpler namespace Application\Controller; class IndexControllerTest extends Framework\TestCase { protected function setUp() { $this->controller = new IndexController(); $this->controller->setServiceLocator(new ServiceManager()); } public function testloadForm() { $response = $this->controller->indexAction(); $this->assertIntanceOf('Form', $response->getVariable(form'')); } }

Slide 48

Slide 48 text

Tests :: be simpler setData(array( 'name' => 'https://plus.google.com/108793379252097909860/posts' )); $this->assertTrue($form->isValid()); } }

Slide 49

Slide 49 text

Tests :: structure ├── module | ├── Api | | └── tests | ├── Application | | └── tests | └── Registration | └── tests └── phpunit.xml

Slide 50

Slide 50 text

Tests :: phpunit.xml module/Api/tests module/Application/tests

Slide 51

Slide 51 text

Tests :: bootstrap.php

Slide 52

Slide 52 text

Distribution

Slide 53

Slide 53 text

Composer # curl -s https://getcomposer.org/installer | php # composer.phar install # composer.phar update

Slide 54

Slide 54 text

Composer { "name": "necromant2005/cdn", "type": "library", "description": "my super project", "keywords": ["my super project"], "homepage": "https://github.com/necromant2005/cdn.git", "authors": [] "repositories": [], "require": {}, "autoload": { "psr-0": {}, "classmap": {} } }

Slide 55

Slide 55 text

Composer :: author "authors": [ { "name": "Rostislav Mykhajliw", "email": "[email protected]", "homepage": "https://github.com/necromant2005", "role": "Developer" } ],

Slide 56

Slide 56 text

Composer :: repositories "repositories": [ { "type": "composer", "url": "http://packages.zendframework.com/" }, { "type": "vcs", "url": "https://github.com/necromant2005/cdn" }, { "type": "vcs", "url": "https://github.com/zendframework/ZendService_Twitter" } ],

Slide 57

Slide 57 text

Composer :: require "require": { "php": ">=5.3.0", "necromant2005/cdn": "@dev", "zendframework/zendframework": "2.0.3", "zendframework/zendservice-twitter": "dev- master#1708df156b64cff0c79e3db783476b7900714db5", "zendframework/zendgdata": "2.*", "zendframework/zendrest": "2.0.*", },

Slide 58

Slide 58 text

Distribution Composer: https://packagist.org/ ZF2 modules: http://modules.zendframework.com/ Private(internal company usage): https://github.com/composer/packagist

Slide 59

Slide 59 text

CI (continuous integration)

Slide 60

Slide 60 text

Travis-ci + free (but also paid solution for ) + works with github (only) + clear environment every run + multiple php version + easy integration and notification + all configuration in one file .travis.yml But only execution limit - 35 min

Slide 61

Slide 61 text

Travis-ci language: php php: - 5.4 before_script: - sudo apt-get update -qq - sudo apt-get install -qq memcached memcachedb tidy - printf "\n"| sudo pecl install memcache - composer install notifications: email: - [email protected] https://github.com/necromant2005/test/blob/master/.travis.yml

Slide 62

Slide 62 text

Acceptable tests :: Selenium?

Slide 63

Slide 63 text

Casper.js + Full JS/Ajax supports + Phantomjs as backend + A lots of helpers and tools + Do NOT open browser windows + Simple readable code and runner + alert/js error handling http://casperjs.org/index.html

Slide 64

Slide 64 text

Casperjs var casper = require('casper').create(); casper.start('http://www.google.com/', function() { this.test.assertExists('form[action="/search"]', 'main form is found'); this.fill('form[action="/search"]', { q: 'foo' }, true); }); casper.then(function() { this.test.assertTitle('foo - Recherche Google', 'google title is ok'); }); casper.run(function() { this.test.renderResults(true); });

Slide 65

Slide 65 text

Casperjs http://casperjs.org/installation.html

Slide 66

Slide 66 text

Casperjs :: tips this.capture("true-form.png"); // xpath var xpath = require('casper').selectXPath; this.test.assertSelectorHasText(xpath("//table[@class='table table- striped']/tbody/tr[1]/td[4]"), "20"); // wait ajax for 10 seconds this.waitWhileSelector('div[name="cacheGooglePlus"]', function() { this.echo('div[name="cacheGooglePlus"] is no more!'); }, 10); // click the link this.click('a[href="/metric"]'); // phantomjs object access and get base64 resource attr logo = this.evaluate(function() { return __utils__.getBase64(document.querySelector('img.logo').getAttribute('src')); });

Slide 67

Slide 67 text

Questions? http://framework.zend.com/manual/2.0/en/user-guide/unit-testing.html http://about.travis-ci.org/docs/user/languages/php/ http://casperjs.org/testing.html http://getcomposer.org/doc/00-intro.md