building single page application (spa) with symfony2 and angularjs

47
Building SPA with Symfony2 and AngularJS Antonio Perić-Mažar 02.10.2014, ZgPHP Conference 2014 https://joind.in/12007

Upload: antonio-peric

Post on 11-Jan-2015

407 views

Category:

Technology


8 download

DESCRIPTION

Forget about classic website where UX is not so important. We are living in time where usability is one of the important thing if you are building some business client oriented web service. How to connect Symfony2 as backend and AngularJS as frontend solution? What are best practices? What are disadvantageous? How to take best from both worlds? These are topics I will cover in my talk with real examples.

TRANSCRIPT

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• [email protected]• 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

etc ... ...Detailed comparison: http://vschart.com/compare/angularjs/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 wsSimpler 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 SF2Locastic\Bundle\TodoBundle\Entity\Todo:

# 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 SF2fos_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:

'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404

'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT

messages:

'Symfony\Component\Routing\Exception\ResourceNotFoundException': 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" = "Locastic\Bundle\FeedBundle\Entity\Story" * }, * statusCodes = { * 200 = "Returned when successful", * 400 = "Returned when bad parameters given" * } * ) * * @Rest\View( * 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 %}

TemplatingUsing 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 routesClient side:

ngRoute

independent since Angular 1.1.6

hashbang #! & HTML5 mode

<base href="/">

$locationProvider

.html5Mode(true)

.hashPrefix('!');

Also good for SEO!

Managing routeshttp://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 routesThink 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 }

options:

expose: true

It's as simple as calling:

→ Routing.generate('my_route_to_expose_with_defaults', {'page': 3})

Or if you want to generate absolute URLs:

→ Routing.generate('my_route_to_expose_with_defaults', {'page': 3}, true)

More info: https://github.com/FriendsOfSymfony/FOSJsRoutingBundle

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

FormssfugDemoApp.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>

FormssfugDemoApp.directive('ngToDoForm', function() {

return { restrict: 'E', templateUrl: '/api/form/show.html' }});

FormssfugDemoApp.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 policyFor 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 CorsCORS (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 ) …One can easily implement the CORS mechanism in Symfony2 with the bundle NelmioCORSBundle . This bundle will add seamlessly headers "CORS" in outgoing HTTP requests from our Symfony application.

Testing

Symfony and AngularJS are designed to test. So write test

Behat

PHPUnit

PHPSpec

Jasmine

Or whatever you want just write tests

SummaryThe 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 Symfony are pretty cool

Take advantage of the authentication feature of Symfony. You can used the same authenticated session to the API for the application, too. No need to use Oauth.

And remember

Keep controllers small and stupid, master Dependency injection, delegate to services and events.

Thank you!

QA

Please rate my talkhttps://joind.in/12007

follow me on twitter: @antonioperic