Download as pdf or txt
Download as pdf or txt
You are on page 1of 47

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

Detailed
etc 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 ws
Simpler than SOAP & WSDL
Resource-oriented (URI)

Principles:

HTTP methods (idempotent & not)


stateless
directory structure-like URIs
XML or JSON (or XHTML)
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
Locastic\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 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:

'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 %}
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
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

You might also like