Building SPA with Symfony2 and AngularJS Antonio Perić-Mažar 02.10.2014, ZgPHP Conference 2014 https://joind.in/12007
About me • Antonio Perić-Mažar, mag. ing. comp. • CEO @ locastic • Software developer, Symfony2 • Sylius Awesome Contributor :) • www.locastic.com • antonio@locastic.com • twitter: @antonioperic
Who we are? • locastic (www.locastic.com) • Web and mobile development • UI/UX design • Located in Split, Croatia
Our works?
Symfony2 AngularJS Usage, language Backend, PHP Frontend, Javascript Dependency Injection Yes Yes Templating Twig HTML Form component Yes Yes Routing component Yes Yes MVC Yes Yes Testable Yes Yes Services Yes Yes Events Yes Yes i18n Yes Yes Dependency management Yes Yes Deteatciled comparison: http://vsch.a..rt.com/compare/a..n. gularjs/vs/symfony
SPA  Aka SPI (Single Page interface)  desktop apps UX  HTML / JS / CSS / etc in single page load  fast  AJAX and XHR
UI == APP
SPA Arhitecture Backend (rest api) with Symfony2 Frontend with AngularJs Separation or combination?
SPA Arhitecture Backend (rest api) with Symfony2 Frontend with AngularJs Separation or combination?
RESTful ws Simpler than SOAP & WSDL Resource-oriented (URI) Principles: HTTP methods (idempotent & not) stateless directory structure-like URIs XML or JSON (or XHTML) GET (vs HEAD), POST, PUT (vs PATCH), DELETE, OPTIONS
Building Rest API with SF2 There is bundle for everything in Sf2. Right? So lets use some of them!
Building Rest API with SF2 What we need? JMSSerializerBundle FOSRestBundle NelmioApiDocBundle
Building Rest API with SF2 JMSSerializerBundle (de)serialization via annotations / YAML / XML / PHP integration with the Doctrine ORM handling of other complex cases (e.g. circular references)
Building Rest API with SF2 LocasticBundleTodoBundleEntityTodo: # exclusion_policy: ALL exclusion_policy: NONE properties: # description: # expose: true createdAt: # expose: true exclude: true deadline: type: DateTime<'d.m.Y. H:i:s'> # expose: true done: # expose: true serialized_name: status
Building Rest API with SF2 fos_rest: disable_csrf_role: ROLE_API param_fetcher_listener: true view: view_response_listener: 'force' formats: xml: true json: true templating_formats: html: true format_listener: rules: - { path: ^/, priorities: [ html, json, xml ], fallback_format: ~, prefer_extension: true } exception: codes: 'SymfonyComponentRoutingExceptionResourceNotFoundException': 404 'DoctrineORMOptimisticLockException': HTTP_CONFLICT messages: 'SymfonyComponentRoutingExceptionResourceNotFoundException': true allowed_methods_listener: true access_denied_listener: json: true body_listener: true
Building Rest API with SF2 /** * @ApiDoc( * resource = true, * description = "Get stories from users that you follow (newsfeed)", * section = "Feed", * output={ * "class" = "LocasticBundleFeedBundleEntityStory" * }, * statusCodes = { * 200 = "Returned when successful", * 400 = "Returned when bad parameters given" * } * ) * * @RestView( * serializerGroups = {"feed"} * ) */ public function getFeedAction() { $this->get('locastic_auth.auth.handler')->validateRequest($this->get('request')); return $this->getDoctrine()->getRepository('locastic.repository.story')->getStories($this- >get('request')->get('me')); }
Building Rest API with SF2
Templating TWIG <3
Templating <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %} <!-- Place favicon.ico and apple-touch-icon.png in the root directory --> {#<link rel="stylesheet" href="{{ asset('css/normalize.css') }}">#} <link rel="stylesheet" href="{{ asset('css/main.css') }}"> <!-- load bootstrap and fontawesome via CDN --> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" /> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css" /> <script src="{{ asset('js/vendor/modernizr-2.6.2.min.js') }}"></script> {% endblock %} <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" /> </head> <body> {% block body %}{% endblock %} {% block javascripts %} <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script> <script src="https://code.angularjs.org/1.2.16/angular-route.min.js"></script> <script src="{{ asset('js/main.js') }}"></script> {% endblock %} </body> </html>
Templating Problem: {{ interpolation tags }} - used both by twig and AngularJS
Templating {% verbatim %} {{ message }} {% endverbatim %}
Templating var phpDayDemoApp = angular.module('phpDayDemoApp', [], function($interpolateProvider) { $interpolateProvider.startSymbol('[['); $interpolateProvider.endSymbol(']]'); }); Now we can use {% block content %} [[ message ]] {# rendered by AngularJS #} {% end block %} Tweak Twig lexer delimiters? Bad idea.
Templating Using assetic for minimize {% javascripts "js/angular-modules/mod1.js" "#s/angular-modules/mod2.js" "@AngBundle/Resources/public/js/controller/*.js" output="compiled/js/app.js" %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %}
Templating Using assetic for minimize Since Angular infers the controller's dependencies from the names of arguments to the controller's constructor function, if you were to minify the JavaScript code for PhoneListCtrl controller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly. Use an inline annotation where, instead of just providing the function, you provide an array. This array contains a list of the service names, followed by the function itself. function PhoneListCtrl($scope, $http) {...} phonecatApp.controller('phpDayCtrl', ['$scope', '$http', PhoneListCtrl]);
Templating Frontend developers still can use their tools like: Bower Grunt Etc.
Managing routes Client side: ngRoute independent since Angular 1.1.6 hashbang #! & HTML5 mode <base href="/"> $locationProvider .html5Mode(true) .hashPrefix('!'); Also good for SEO!
Managing routes http://localhost/todos http://localhost/#todos Resolving conflicts Fallback, managing 404 angular: path: '/{route}' defaults: { _controller: LocasticAngularBundle:Default:index} requirements: route: ".+"
Managing routes – client side // module configuration...$routeProvider.when('/todos/show/:id', { templateUrl : 'todo/show', controller : 'todoController' }) // receive paramssfugDemoApp.controller('todoController', function($scope, $http, $routeParams){ $scope.todo = {}; $http .get('/api/todo/show/' + $routeParams.id) .success(function(data){ $scope.todo = data['todo']; }); });
Managing routes Think of using FOSJsRoutingBundle for Frontend route managament <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script> <script src="{{ path('fos_js_routing_js', {"callback": "fos.Router.setData"}) }}"></script> my_route_to_expose_with_defaults: pattern: /blog/{page} defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
Managing routes – server side locastic_rest_todo_getall: pattern: /api/get-all defaults: _controller: LocasticRestBundle:Todo:getAll locastic_rest_todo_create: pattern: /api/create defaults: _controller: LocasticRestBundle:Todo:create locastic_rest_todo_show: pattern: /api/show/{id} defaults: _controller: LocasticRestBundle:Todo:show
Translations AngularJS has its own translation system I18N/L10N . But it might be interesting to monitor and centralize translations from your backend Symfony. JMSTranslationBundle
Forms Symfony Forms <3 We don't want to throw them away Build custom directive
Forms sfugDemoApp.directive('ngToDoForm', function() { return { restrict: 'E', template: '<div class="todoForm">Form will be!</div>' } }); 'A' - <span ng-sparkline></span> 'E' - <ng-sparkline></ng-sparkline> 'C' - <span class="ng-sparkline"></span> 'M' - <!-- directive: ng-sparkline → Usage of directive in HTML: <ng-to-do-form></ng-to-do-form>
Forms sfugDemoApp.directive('ngToDoForm', function() { return { restrict: 'E', templateUrl: '/api/form/show.html' } });
Forms sfugDemoApp.directive('ngToDoForm', function() { return { restrict: 'E', templateUrl: '/api/form/show.html' } }); locastic_show_form: pattern: /form/show.html defaults: _controller: LocasticWebBundle:Default:renderForm public function renderFormAction() { $form = $this->createForm(new TodoType()); return $this->render('LocasticWebBundle::render_form.html.twig', array( 'form' => $form->createView() )); }
Forms Suprise!!!
Forms Template behind directive <form class="form-inline" role="form" style="margin-bottom: 30px;"> Create new todo: <div class="form-group"> {{ form_label(form.description) }} {{ form_widget(form.description, {'attr': {'ng-model': 'newTodo.description', 'placeholder': 'description', 'class': 'form-control'}}) }} </div> <div class="form-group"> <label class="sr-only" for="deadline">Deadline</label> <input type="text" class="form-control" id="deadline" placeholder="deadline (angular-ui)" ng-model=" newTodo.deadline"> </div> <input type="button" class="btn btn-default" ng-click="addNew()" value="add"/> </form>
Submitting forms? When the AngularJS $http service POSTs data the header application/x-www-form-urlencoded is never set (unlike jQuery’s $.ajax()). Also, the $http data is not serialized when sent. Both of these facts mean that the $_POST variable is never set properly by php. Without the $_POST variable Symfony’s built in form handling cannot be used. The fix is actually pretty simple:  angular needs to forced into setting a header  the data needs to be serialized  and the data needs to be normalized into a multidimensional array.
Submitting forms? var postData = { formtype_name: { id: some_id, name: some_name } }; $http({ method: "POST", url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: $.param(postData) });
Same-origin policy For security reasons, web browsers prevent JavaScript to Ajax requests (XMLHttpRequest) to other areas ( Same-origin policy ). An example of exception thrown by the browser: XMLHttpRequest cannot load http://api.mondomaine.com/v1/maressource.json. Invalid HTTP status code 405  Using JSOP (Json with Padding) – easy with FOSRestApi  Configure server (simple and stupid) <VirtualHost *:80> ServerName mon-appli-angular.com DocumentRoot /var/www/some-ng-app/ Alias /api /var/www/some-ng-app/ <Directory xxxx> </Directory> </VirtualHost> </VirtualHost>  Use Cors CORS (Cross-origin resource sharing) is an elegant and standardized response to allow Cross-domain requests. Be careful though, the CORS mechanism is not supported by all browsers (guess which ) …
Testing Symfony and AngularJS are designed to test. So write test Behat PHPUnit PHPSpec Jasmine … Or whatever you want just write tests
Summary The cleanest way is to separate backend and frontend. But there is some advantages to use both together. Twig + HTML works well. Assetic Bundle is very useful to minify bunches of Javascript files used by AngularJs Translation in the template. the data in the API payload does not need translation in most cases. Using Symfony I18N support for the template makes perfect sense. Loading of Option lists. Say you have a country list with 200+ options. You can build an API to populate a dynamic dropdown in angularjs, but as these options are static, you can simply build that list in twig. Forms in
And remember Keep controllers small and stupid, master Dependency injection, delegate to services and events.
Thank you!
QA Please rate my talk https://joind.in/12007 follow me on twitter: @antonioperic

Building Single Page Application (SPA) with Symfony2 and AngularJS

  • 1.
    Building SPA withSymfony2 and AngularJS Antonio Perić-Mažar 02.10.2014, ZgPHP Conference 2014 https://joind.in/12007
  • 2.
    About me •Antonio Perić-Mažar, mag. ing. comp. • CEO @ locastic • Software developer, Symfony2 • Sylius Awesome Contributor :) • www.locastic.com • antonio@locastic.com • twitter: @antonioperic
  • 3.
    Who we are? • locastic (www.locastic.com) • Web and mobile development • UI/UX design • Located in Split, Croatia
  • 4.
  • 6.
    Symfony2 AngularJS Usage,language Backend, PHP Frontend, Javascript Dependency Injection Yes Yes Templating Twig HTML Form component Yes Yes Routing component Yes Yes MVC Yes Yes Testable Yes Yes Services Yes Yes Events Yes Yes i18n Yes Yes Dependency management Yes Yes Deteatciled comparison: http://vsch.a..rt.com/compare/a..n. gularjs/vs/symfony
  • 7.
    SPA  AkaSPI (Single Page interface)  desktop apps UX  HTML / JS / CSS / etc in single page load  fast  AJAX and XHR
  • 9.
  • 10.
    SPA Arhitecture Backend(rest api) with Symfony2 Frontend with AngularJs Separation or combination?
  • 11.
    SPA Arhitecture Backend(rest api) with Symfony2 Frontend with AngularJs Separation or combination?
  • 12.
    RESTful ws Simplerthan SOAP & WSDL Resource-oriented (URI) Principles: HTTP methods (idempotent & not) stateless directory structure-like URIs XML or JSON (or XHTML) GET (vs HEAD), POST, PUT (vs PATCH), DELETE, OPTIONS
  • 13.
    Building Rest APIwith SF2 There is bundle for everything in Sf2. Right? So lets use some of them!
  • 14.
    Building Rest APIwith SF2 What we need? JMSSerializerBundle FOSRestBundle NelmioApiDocBundle
  • 15.
    Building Rest APIwith SF2 JMSSerializerBundle (de)serialization via annotations / YAML / XML / PHP integration with the Doctrine ORM handling of other complex cases (e.g. circular references)
  • 16.
    Building Rest APIwith SF2 LocasticBundleTodoBundleEntityTodo: # exclusion_policy: ALL exclusion_policy: NONE properties: # description: # expose: true createdAt: # expose: true exclude: true deadline: type: DateTime<'d.m.Y. H:i:s'> # expose: true done: # expose: true serialized_name: status
  • 17.
    Building Rest APIwith SF2 fos_rest: disable_csrf_role: ROLE_API param_fetcher_listener: true view: view_response_listener: 'force' formats: xml: true json: true templating_formats: html: true format_listener: rules: - { path: ^/, priorities: [ html, json, xml ], fallback_format: ~, prefer_extension: true } exception: codes: 'SymfonyComponentRoutingExceptionResourceNotFoundException': 404 'DoctrineORMOptimisticLockException': HTTP_CONFLICT messages: 'SymfonyComponentRoutingExceptionResourceNotFoundException': true allowed_methods_listener: true access_denied_listener: json: true body_listener: true
  • 18.
    Building Rest APIwith SF2 /** * @ApiDoc( * resource = true, * description = "Get stories from users that you follow (newsfeed)", * section = "Feed", * output={ * "class" = "LocasticBundleFeedBundleEntityStory" * }, * statusCodes = { * 200 = "Returned when successful", * 400 = "Returned when bad parameters given" * } * ) * * @RestView( * serializerGroups = {"feed"} * ) */ public function getFeedAction() { $this->get('locastic_auth.auth.handler')->validateRequest($this->get('request')); return $this->getDoctrine()->getRepository('locastic.repository.story')->getStories($this- >get('request')->get('me')); }
  • 19.
  • 20.
  • 21.
    Templating <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %} <!-- Place favicon.ico and apple-touch-icon.png in the root directory --> {#<link rel="stylesheet" href="{{ asset('css/normalize.css') }}">#} <link rel="stylesheet" href="{{ asset('css/main.css') }}"> <!-- load bootstrap and fontawesome via CDN --> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" /> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css" /> <script src="{{ asset('js/vendor/modernizr-2.6.2.min.js') }}"></script> {% endblock %} <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" /> </head> <body> {% block body %}{% endblock %} {% block javascripts %} <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script> <script src="https://code.angularjs.org/1.2.16/angular-route.min.js"></script> <script src="{{ asset('js/main.js') }}"></script> {% endblock %} </body> </html>
  • 22.
    Templating Problem: {{interpolation tags }} - used both by twig and AngularJS
  • 23.
    Templating {% verbatim%} {{ message }} {% endverbatim %}
  • 24.
    Templating var phpDayDemoApp= angular.module('phpDayDemoApp', [], function($interpolateProvider) { $interpolateProvider.startSymbol('[['); $interpolateProvider.endSymbol(']]'); }); Now we can use {% block content %} [[ message ]] {# rendered by AngularJS #} {% end block %} Tweak Twig lexer delimiters? Bad idea.
  • 25.
    Templating Using asseticfor minimize {% javascripts "js/angular-modules/mod1.js" "#s/angular-modules/mod2.js" "@AngBundle/Resources/public/js/controller/*.js" output="compiled/js/app.js" %} <script type="text/javascript" src="{{ asset_url }}"></script> {% endjavascripts %}
  • 26.
    Templating Using asseticfor minimize Since Angular infers the controller's dependencies from the names of arguments to the controller's constructor function, if you were to minify the JavaScript code for PhoneListCtrl controller, all of its function arguments would be minified as well, and the dependency injector would not be able to identify services correctly. Use an inline annotation where, instead of just providing the function, you provide an array. This array contains a list of the service names, followed by the function itself. function PhoneListCtrl($scope, $http) {...} phonecatApp.controller('phpDayCtrl', ['$scope', '$http', PhoneListCtrl]);
  • 27.
    Templating Frontend developersstill can use their tools like: Bower Grunt Etc.
  • 28.
    Managing routes Clientside: ngRoute independent since Angular 1.1.6 hashbang #! & HTML5 mode <base href="/"> $locationProvider .html5Mode(true) .hashPrefix('!'); Also good for SEO!
  • 29.
    Managing routes http://localhost/todos http://localhost/#todos Resolving conflicts Fallback, managing 404 angular: path: '/{route}' defaults: { _controller: LocasticAngularBundle:Default:index} requirements: route: ".+"
  • 30.
    Managing routes –client side // module configuration...$routeProvider.when('/todos/show/:id', { templateUrl : 'todo/show', controller : 'todoController' }) // receive paramssfugDemoApp.controller('todoController', function($scope, $http, $routeParams){ $scope.todo = {}; $http .get('/api/todo/show/' + $routeParams.id) .success(function(data){ $scope.todo = data['todo']; }); });
  • 31.
    Managing routes Thinkof using FOSJsRoutingBundle for Frontend route managament <script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script> <script src="{{ path('fos_js_routing_js', {"callback": "fos.Router.setData"}) }}"></script> my_route_to_expose_with_defaults: pattern: /blog/{page} defaults: { _controller: AcmeBlogBundle:Blog:index, page: 1 }
  • 32.
    Managing routes –server side locastic_rest_todo_getall: pattern: /api/get-all defaults: _controller: LocasticRestBundle:Todo:getAll locastic_rest_todo_create: pattern: /api/create defaults: _controller: LocasticRestBundle:Todo:create locastic_rest_todo_show: pattern: /api/show/{id} defaults: _controller: LocasticRestBundle:Todo:show
  • 33.
    Translations AngularJS hasits own translation system I18N/L10N . But it might be interesting to monitor and centralize translations from your backend Symfony. JMSTranslationBundle
  • 34.
    Forms Symfony Forms<3 We don't want to throw them away Build custom directive
  • 35.
    Forms sfugDemoApp.directive('ngToDoForm', function(){ return { restrict: 'E', template: '<div class="todoForm">Form will be!</div>' } }); 'A' - <span ng-sparkline></span> 'E' - <ng-sparkline></ng-sparkline> 'C' - <span class="ng-sparkline"></span> 'M' - <!-- directive: ng-sparkline → Usage of directive in HTML: <ng-to-do-form></ng-to-do-form>
  • 36.
    Forms sfugDemoApp.directive('ngToDoForm', function(){ return { restrict: 'E', templateUrl: '/api/form/show.html' } });
  • 37.
    Forms sfugDemoApp.directive('ngToDoForm', function(){ return { restrict: 'E', templateUrl: '/api/form/show.html' } }); locastic_show_form: pattern: /form/show.html defaults: _controller: LocasticWebBundle:Default:renderForm public function renderFormAction() { $form = $this->createForm(new TodoType()); return $this->render('LocasticWebBundle::render_form.html.twig', array( 'form' => $form->createView() )); }
  • 38.
  • 39.
    Forms Template behinddirective <form class="form-inline" role="form" style="margin-bottom: 30px;"> Create new todo: <div class="form-group"> {{ form_label(form.description) }} {{ form_widget(form.description, {'attr': {'ng-model': 'newTodo.description', 'placeholder': 'description', 'class': 'form-control'}}) }} </div> <div class="form-group"> <label class="sr-only" for="deadline">Deadline</label> <input type="text" class="form-control" id="deadline" placeholder="deadline (angular-ui)" ng-model=" newTodo.deadline"> </div> <input type="button" class="btn btn-default" ng-click="addNew()" value="add"/> </form>
  • 40.
    Submitting forms? Whenthe AngularJS $http service POSTs data the header application/x-www-form-urlencoded is never set (unlike jQuery’s $.ajax()). Also, the $http data is not serialized when sent. Both of these facts mean that the $_POST variable is never set properly by php. Without the $_POST variable Symfony’s built in form handling cannot be used. The fix is actually pretty simple:  angular needs to forced into setting a header  the data needs to be serialized  and the data needs to be normalized into a multidimensional array.
  • 41.
    Submitting forms? varpostData = { formtype_name: { id: some_id, name: some_name } }; $http({ method: "POST", url: url, headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, data: $.param(postData) });
  • 42.
    Same-origin policy Forsecurity reasons, web browsers prevent JavaScript to Ajax requests (XMLHttpRequest) to other areas ( Same-origin policy ). An example of exception thrown by the browser: XMLHttpRequest cannot load http://api.mondomaine.com/v1/maressource.json. Invalid HTTP status code 405  Using JSOP (Json with Padding) – easy with FOSRestApi  Configure server (simple and stupid) <VirtualHost *:80> ServerName mon-appli-angular.com DocumentRoot /var/www/some-ng-app/ Alias /api /var/www/some-ng-app/ <Directory xxxx> </Directory> </VirtualHost> </VirtualHost>  Use Cors CORS (Cross-origin resource sharing) is an elegant and standardized response to allow Cross-domain requests. Be careful though, the CORS mechanism is not supported by all browsers (guess which ) …
  • 43.
    Testing Symfony andAngularJS are designed to test. So write test Behat PHPUnit PHPSpec Jasmine … Or whatever you want just write tests
  • 44.
    Summary The cleanestway is to separate backend and frontend. But there is some advantages to use both together. Twig + HTML works well. Assetic Bundle is very useful to minify bunches of Javascript files used by AngularJs Translation in the template. the data in the API payload does not need translation in most cases. Using Symfony I18N support for the template makes perfect sense. Loading of Option lists. Say you have a country list with 200+ options. You can build an API to populate a dynamic dropdown in angularjs, but as these options are static, you can simply build that list in twig. Forms in
  • 45.
    And remember Keepcontrollers small and stupid, master Dependency injection, delegate to services and events.
  • 46.
  • 47.
    QA Please ratemy talk https://joind.in/12007 follow me on twitter: @antonioperic