Using Ember.js in Zend Framework 2 Application
Ember.js is one of the javascript frameworks that adopt Single-Page Application principles. When working with Zend Framework 2 application, we can terminate 2 step view process to just render the view (not the layout) when the request that comes is XmlHttpRequest. We can do it in our Module.php like the following code :
use Zend\View\Model\ViewModel; class Module { public function onBootstrap($e) { $eventManager = $e->getApplication()->getEventManager(); $sharedEvents = $eventManager->getSharedManager(); $sharedEvents->attach('Zend\Mvc\Controller\AbstractActionController', 'dispatch', function($e) { $result = $e->getResult(); if ($result instanceof ViewModel) { $result->setTerminal($e->getRequest()->isXmlHttpRequest()); } }); // ... } // ... }
At this post, I will try to make a 3 static page : ‘Home’, ‘About’, and ‘Contact’ in Application module. Let’s first create the navigation :
// config/autoload/navigation.global.php return [ 'navigation' => [ 'default' => [ [ 'label' => 'Home', 'route' => 'home' ], [ 'label' => 'About', 'route' => 'about', ], [ 'label' => 'Contact', 'route' => 'contact', ], ], ], ];
We can then register the navigation service :
// module/Application/config/module.config.php // ... 'service_manager' => [ 'factories' => [ 'Navigation' => 'Zend\Navigation\Service\DefaultNavigationFactory', ], ] // ...
From the navigation registered above, we can create 3 controller like this :
1. IndexController
// module/Application/src/Application/Controller/IndexController.php namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class IndexController extends AbstractActionController { public function indexAction() { return new ViewModel(); } }
2. AboutController
// module/Application/src/Application/Controller/AboutController.php namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class AboutController extends AbstractActionController { public function indexAction() { return new ViewModel(); } }
3. ContactController
// module/Application/src/Application/Controller/ContactController.php namespace Application\Controller; use Zend\Mvc\Controller\AbstractActionController; use Zend\View\Model\ViewModel; class ContactController extends AbstractActionController { public function indexAction() { return new ViewModel(); } }
Controllers created, we can register it into module.config.php under ‘router’ and ‘controllers’ :
// module/Application/config/module.config.php // ... 'router' => [ 'routes' => [ 'about' => [ 'type' => 'Literal', 'options' => [ 'route' => '/about', 'defaults' => [ 'controller' => 'Application\Controller\About', 'action' => 'index', ], ], ], 'contact' => [ 'type' => 'Literal', 'options' => [ 'route' => '/contact', 'defaults' => [ 'controller' => 'Application\Controller\Contact', 'action' => 'index', ], ], ], ], ], 'controllers' => [ 'invokables' => [ 'Application\Controller\Index' => 'Application\Controller\IndexController', 'Application\Controller\Contact' => 'Application\Controller\ContactController', 'Application\Controller\About' => 'Application\Controller\AboutController', ], ], // ...
I assume you have using default ZendSkeletonApplication so the IndexController route already defined. Ok, now we need to fill the view.
1 index view ( same as the skeleton app view/application/index/index.phtml )
2 about view
// module/Application/view/application/about/index.phtml <h1>About Me</h1> <p> I'm a web developer. </p>
3 contact view
// module/Application/view/application/contact/index.phtml <h1>Contact Me</h1> <p> You can contact me via <a href="mailto: foo@bar.baz.com">foo@bar.baz.com</a> </p>
Yup, Let’s go to javascript side.
First, require Ember.js in bower.json and install it :
// bower.json { "name":"ZF2 App with Ember Demo", "dependencies": { "ember": "1.*" } }
Configure it to be installed in public/js
folder in .bowerrc
.
// .bowerrc { "directory": "public/js", "json": "bower.json" }
Run bower install :
bower install
For Ember 1.10, As we will need template compilation, We need to require ember-template-compiler
like shown in here. To make it included, we need to require it in layout.phtml in headScript()
view helper :
echo $this->headScript() ->prependFile($this->basePath() . '/js/bootstrap.min.js') ->prependFile($this->basePath() . '/js/jquery.min.js') ->prependFile($this->basePath() . '/js/respond.min.js', 'text/javascript', array('conditional' => 'lt IE 9',)) ->prependFile($this->basePath() . '/js/html5shiv.js', 'text/javascript', array('conditional' => 'lt IE 9',)) // ember js dependencies ->appendFile($this->basePath() . '/js/ember/ember-template-compiler.js') ->appendFile($this->basePath() . '/js/ember/ember.min.js'); ;
Now, we need to create a new javascript file for its application specific requirement, I name it app.js :
// public/js/app.js App = Ember.Application.create(); App.Router.map(function() { this.resource('home', { path: '/' }); this.resource('about'); this.resource('contact'); });
Layout
We need to replace :
<?php echo $this->content; ?>
with
{{outlet}}
And wrap it in <script type="text/x-handlebars">
:
// module/Application/view/layout/layout.phtml // ... <script type="text/x-handlebars"> <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse"> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="<?php echo $this->url('home') ?>"><img src="<?php echo $this->basePath('img/zf2-logo.png') ?>" alt="Zend Framework 2"/> <?php echo $this->translate('Skeleton Application') ?></a> </div> <div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <?php $navigationContainer = $this->navigation('navigation')->getContainer(); foreach($navigationContainer as $page) { ?> <li> {{#link-to '<?php echo $page->get('route'); ?>'}} <?php echo $page->get('label'); ?> {{/link-to}} </li> <?php } ?> </ul> </div><!--/.nav-collapse --> </div> </nav> <div class="container"> {{outlet}} </div> </script>
We need to require our public/js/app.js
in footer :
<?php $script = $this->inlineScript(); $script->appendFile($this->basePath() . '/js/app.js'); echo $script; ?> </body> // means in the footer!
Yup, time to make ajax works, modify public/js/app.js
by adding the following codes :
// public/js/app.js // ... App.HomeRoute = Ember.Route.extend({ beforeModel: function() { return $.ajax({ url: '/' }) .then(function(response) { Ember.TEMPLATES.home = Ember.Handlebars.compile(response); }); } }); App.AboutRoute = Ember.Route.extend({ beforeModel: function() { return $.ajax({ url: '/about' }) .then(function(response) { Ember.TEMPLATES.about = Ember.Handlebars.compile(response); }); } }); App.ContactRoute = Ember.Route.extend({ beforeModel: function() { return $.ajax({ url: '/contact' }) .then(function(response) { Ember.TEMPLATES.contact = Ember.Handlebars.compile(response); }); } }); // to remove # ( hash ) in url if (window.history && window.history.pushState) { App.Router.reopen({ location: 'history' }); }
Great! If everything ok, then your ZF2 app with Ember.js should work like a magic! The requested page loaded without refreshing the page!.
Bonus
You can make ‘li’ under ‘ul’ for navigation class setted to active when it in the page, with create new ‘li’ component, we can add it in public/js/app.js
// public/js/app.js // ... App.LinkLiComponent = Ember.Component.extend({ tagName: 'li', classNameBindings: ['active'], active: function() { return this.get('childViews').anyBy('active'); }.property('childViews.@each.active') }); Ember.Handlebars.helper('link-li', App.LinkLiComponent);
And then, we can modify the looks like :
// module/Application/view/layout/layout.phtml // ... <?php $navigationContainer = $this->navigation('navigation')->getContainer(); foreach($navigationContainer as $page) { ?> {{#link-li}} {{#link-to '<?php echo $page->get('route'); ?>'}} <?php echo $page->get('label'); ?> {{/link-to}} {{/link-li}} <?php } ?>
Ok, I hope it useful for you ;). Want to grab the sourcecode ? Clone from my repository https://github.com/samsonasik/zfember 😉
Images :
1. http://www.gravatar.com/avatar/0cf15665a9146ba852bf042b0652780a?s=200
References :
1. http://emberjs.com/
2. http://code.tutsplus.com/tutorials/getting-into-ember-js-part-2–net-31132
3. http://code.tutsplus.com/tutorials/getting-into-emberjs-part-3–net-31394
4. http://stackoverflow.com/questions/17792280/ember-live-uploading-templates
5. http://stackoverflow.com/questions/19871265/ember-js-with-external-handlebars-template
6. http://stackoverflow.com/questions/14328295/how-do-i-bind-to-the-active-class-of-a-link-using-the-new-ember-router
7. http://en.wikipedia.org/wiki/Single-page_application
8. http://emberjs.com/blog/2015/02/07/ember-1-10-0-released.html
Testing Lazy Load with ReflectionClass
One of the benefits by using ReflectionClass is when dealing with testing Lazy load. The flow is get the property, use its instance as ReflectionProperty to make it accessible, and set the property value via setValue().
Ok, let’s give a try.
I’ve a “Bar” class that looks like the following :
namespace Samsonasik\TutorialLazyLoading; /** * @author Abdul Malik Ikhsan <samsonasik@gmail.com> */ class Bar { /** * @var Foo */ private $foo; /** * Get foo property * @return Foo */ public function getFoo() { if (!$this->foo) { $this->foo = new Foo; return $this->foo; } return $this->foo; } }
When you call getFoo() in unit test with :
use Samsonasik\TutorialLazyLoading\Bar; use Samsonasik\TutorialLazyLoading\Foo; // ... protected function setUp() { $this->bar = new Bar; } public function testGetFooNotInitializedYet() { $this->assertInstanceOf( Foo::class, /** use 'Samsonasik\TutorialLazyLoading\Foo' for PHP <=5.4 **/ $this->bar->getFoo() ); } // ...
, you will get coverage only if (!$this->foo) {
block like this :
Time to do ReflectionClass in action!, add new test for it’s needed.
use ReflectionClass; use Samsonasik\TutorialLazyLoading\Bar; use Samsonasik\TutorialLazyLoading\Foo; // ... protected function setUp() { $this->bar = new Bar; } public function testGetFooNotInitializedYet() { // ... } public function testGetFooWithAlreadyInialized() { $class = new ReflectionClass( Bar::class /** use 'Samsonasik\TutorialLazyLoading\Bar' for PHP <=5.4 **/ ); $property = $class->getProperty('foo'); $property->setAccessible(true); $property->setValue($this->bar, new Foo); $this->assertInstanceOf( Foo::class, /** use 'Samsonasik\TutorialLazyLoading\Foo' for PHP <=5.4 **/ $this->bar->getFoo() ); } // ...
And yay! You now get fully tested :
Note :
As @ocramius suggestion, there is an easier way to do it by calling the method twice ;).
Want to grab it ? You can grab from my repository : https://github.com/samsonasik/TutorialLazyLoading .
References :
1. http://www.mikeyd.com.au/2011/01/20/how-to-use-phps-reflectionclass-to-test-private-methods-and-properties-with-phpunit/
14 comments