Working with symfony2 models in the front end

30
WORKING WITH SYMFONY2 MODELS IN THE FRONT-END (USING JAVASCRIPT) By / RedNose Leiden Sven Hagemann

Transcript of Working with symfony2 models in the front end

WORKING WITH SYMFONY2MODELS IN THE FRONT-END

(USING JAVASCRIPT)

By / RedNose LeidenSven Hagemann

ABOUT MESVEN CHRISTIAAN HAGEMANN (1986)

2009 until > 9000;

2004 until 2009;

2001 until 2004;

WHEN TO USE MODELS INTHE FRONT-END?

AVARAGE GOVERNMENT COMPUTER BROWSER:

WHEN TO USE MODELS INTHE FRONT-END?

When developing for a modern browser (>IE8, FF 3.5)

When building a single page application(SPA) for desktop

When building a single page application formobile devices

WHEN TO USE MODELS INTHE FRONT-END?

HYBRID SITUATION

Endusers(website front-end)

Administrators(website back-end)

HTML / CSS - Twig Symfony2 Entities Fancy Javascript / HTML5

WHEN TO USE MODELS INTHE FRONT-END?

FROM PROGRESSIVE ENHANCEMENT TO BUILDINGACTUAL APPLICATIONS IN JAVASCRIPT

SunSpider 2001 until Q1-2009 Google Trends 2007 until 2014

SO WHY USE MODELS IN THEFRONT-END?

MODERN WEBDEVELOPMENT PARADIGM

90's early 2000's Today

SO WHY USE MODELS IN THEFRONT-END?

Code structureMaintainability

Working in a team of front-end developers

Clear seperation between front and back-end (developers)

The front-end and back-end share a common data model

StatefulAbility to synchronise state from and to the back-end

Less data usage. (Important for mobile devices)

Less server resources required

Ability to keep data client-side (HTML5 data storage for example)

Bonus: Free API!RESTFul API supporting both XML and JSON

Import / Export entities to XML and JSON

Easy to migrate to newer versions of the data model

HOW DO STATEFULJAVASCRIPT MODELS WORK?

PSEUDO-CODE EXAMPLE: CREATING MODELS// Model() is an object provided by some awesome javascript frameworkvar modelProject = new Model({ name: 'Example project' }), modelTaskPrimary = new Model({ name: 'Make me a sandwich', ready: false }), modelTaskSecondary = new Model({ name: 'Create world peace', ready: false });

// Change todoList property (calls .set() on Model())modelProject.set('todoList', [ modelTaskPrimary, modelTaskSecondary ]);

// Register a callback url for this modelmodelProject.set('routes', { create: Routing.generate('project_create') });

// Persist (Sends a serialized JSON string to a RESTFul api using AJAX)modelProject.save();

HOW DO STATEFULJAVASCRIPT MODELS WORK?

PSEUDO-CODE EXAMPLE: UPDATE MODELvar modelProject = new Model({ routes: { get: Routing.generate('project_get', { id: 1337 }) }});

// Model.load(callback) sends a http call to the 'project_get' route.// If all goes to plan, we recieve a serialized JSON stringmodelProject.load(function () {

if (modelProject.get('todoList').length > 0) { // Get the first task var modelTodoTask = modelProject.get('todoList')[0];

// Change the name of the task (objects are ByRef). // Because the ID of this task is set by the load() it will be // updated when save() is called, otherwise a new task will be created. modelTodoTask.set('name', 'SUDO MAKE ME A SANDWICH!'); }

});

// ... Do some other stuff ...

// At some other point in time.modelProject.save();

HOW DO STATEFULJAVASCRIPT MODELS WORK?

PSEUDO-CODE EXAMPLE: CLASS INHERITANCE// Create prototype model object using inheritancevar ProjectModel = function() {}ProjectModel.prototype = new Model();

// Create a reference to the object parent prototype for superclass calls.ProjectModel.prototype.parent = Model.prototype;

// Overwrite get()ProjectModel.prototype.get = function (name) { if (name === 'routes') { return { create: Routing.generate('project_get'),

get: Routing.generate('project_get', { id: this.parent.get.call(this, 'id'); }) }; }

// Call get() on superclass return this.parent.get.call(this, name);}

HOW DO STATEFULJAVASCRIPT MODELS WORK?

PSEUDO-CODE EXAMPLE: BUSINESS LOGIC// Create prototype model object using inheritancevar ProjectModel = function() {}ProjectModel.prototype = new Model();

/** * Adds a task * * @param {string} name * @return {void} */ProjectModel.prototype.addTask = function (name) { var todoModel = new Model({ name: name, ready: false }), todoModelList = this.get('todoList');

if (todoModelList === null) { todoModelList = []; } todoModelList.push(todoModel)

this.set('todoList', todoModelList);}

REAL WORLD EXAMPLE

LIVE DEMO!(NOTE TO SELF: PRAY TO DEMO-GODS)

CAN WE PLEASE TALK SOMEPHP NOW?

SYMFONY2 ENTITYSERIALIZATIONTHERE IS A BUNDLE FOR THAT!

Exposing your routes to JavaScriptFOSJsRoutingBundle

Make your entities automatically serializeJMSSerializerBundle

Create a RESTFul API, the lazy .. ahum .. easy wayFOSRestBundle

EXPOSING YOUR ROUTES TOJAVASCRIPT

/APP/CONFIG/ROUTING.YML

# If you could go ahead and add the cover to the TPS reports that would be terrificadd_cover_to_tps_report: pattern: /tps_report/add_cover/{reportId} defaults: { _controller: OfficeSpaceTpsBundle:Reports:addCover }

# The important part options: expose: true

FOSRoutingBundle provides Routing.generate();

Generate a JSON object based on exposed routes

EXPOSING YOUR ROUTES TOJAVASCRIPT

INCLUDING FOSJSROUTINGBUNDLE DEPENDENCIES IN<HEADER />

<script type="text/javascript" src="{{ asset('bundles/fosjsrouting/js/router.js') }}">

<script type="text/javascript" src="{{ path('fos_js_routing_js', {"callback": "fos.Router.setData"}) }}">

* Ignore the weird indenting

EXPOSING YOUR ROUTES TOJAVASCRIPT

USAGE

var route = Routing.generate( 'add_cover_to_tps_report', { reportId: 143 });

alert(route);

Adding annotations to your existing entities

http://jmsyst.com/libs/serializer/master/reference/annotations

MAKE YOUR ENTITIESSERIALIZABLE

JMSSERIALIZERBUNDLE ANNOTATIONS

use Rednose\TodoBundle\Model\Task as BaseTask;

// Annotation class provided by the JMSSerializerBundleuse JMS\Serializer\Annotation as Serializer;

class Task extends BaseTask { /** * ... * ... * @Serializer\Type("boolean") * @Serializer\Groups({"details", "file"}) */ protected $ready = false;}

MAKE YOUR ENTITIESSERIALIZABLE

BASIC ENTITY ANNOTATINGuse Rednose\TodoBundle\Model\Project as BaseProject;

use JMS\Serializer\Annotation as Serializer;

/** * @ORM\Entity * @ORM\Table(name="todo_project") * * @Serializer\XmlRoot("project") */class Project extends BaseProject{ /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") * * @Serializer\XmlAttribute * @Serializer\Groups({"details"}) */ protected $id;

/** * @ORM\Column(type="string", length=255) * * @Serializer\XmlAttribute * @Serializer\Type("string") * @Serializer\Groups({"details", "file"}) */ protected $name;

// ...................

MAKE YOUR ENTITIESSERIALIZABLE

RELATIONAL ENTITIESclass Project extends BaseProject{ /** * @ORM\OneToMany( * targetEntity="Task", * orphanRemoval=true, * mappedBy="project", * cascade={"persist", "remove"}) * @ORM\OrderBy({"id" = "ASC"}) * * @Serializer\Groups({"details", "file"}) * @Serializer\SerializedName("tasks") * @Serializer\XmlList(inline = false, entry = "task") */ protected $tasks;

/** * The serializer uses reflection to create objects instead of setters. So make sure you repair * bi-directional relations, otherwise doctrine will remove them when you persist the parent entity. * * @Serializer\PostDeserialize */ public function postDeserialize() { foreach ($this->tasks as $task) { $task->setProject($this); } }}

MAKE YOUR ENTITIESSERIALIZABLEDESERIALIZE AND PERSIST

use JMS\Serializer\DeserializationContext;

class ProjectController {

function updateProjectActions() { $em = $this->getDoctrine()->getManager(); $serializer = $this->get('jms_serializer');

$context = new DeserializationContext(); $context->setGroups(array('details'));

$project = $serializer->deserialize( $this->getRequest()->getContent(), // The JSON send by Javascript. 'Rednose\TodoBundle\Entity\Project', // Base entity namespace. 'json', $context // Format = JSON, Context = details );

$em->persist($project); $em->flush(); }

}

MAKE YOUR ENTITIESSERIALIZABLE

GOOD TO KNOW: EVENTLISTENERSuse JMS\Serializer\EventDispatcher\EventSubscriberInterface;use JMS\Serializer\EventDispatcher\ObjectEvent;

class EntityListener implements EventSubscriberInterface{ protected $user;

public function __construct(ContainerInterface $container) { $this->user = $container->get('security.context')->getToken()->getUser(); }

static public function getSubscribedEvents() { return array( array( 'event' => 'serializer.post_deserialize', 'class' => 'Rednose\TodoBundle\Entity\Task', 'method' => 'onPostDeserialize' ), ); }

public function onPreSerialize(PreSerializeEvent $event) { $task = $event->getobject();

$task->setOwner($this->user); // Javascript is not aware of the session user }}

Replacing the default ObjectConstructor

MAKE YOUR ENTITIESSERIALIZABLE

GOOD TO KNOW: DOCTRINE OBJECTCONSTRUCTOR

If entities are not created using Doctrine they will not be referenced to theexisting entity in the database and therefore when persisted new entities will becreated.

But . . . . JMSSerializerBundle provides us a special ObjectConstructor to solvethis issue.

<container xmlns="..."> <services> <service id="jms_serializer.object_constructor" alias="jms_serializer.doctrine_object_constructor" public="false" /> </services></container>

The Symfony2 way: Convention over configuration is ,there should at least be 19 ways to do the same thing.

Routing.yml

ProjectController.php

RESTFUL API THE EASY WAYBASIC ROUTING ANNOTATIONS

stupid

todo_app: resource: "@RednoseTodoBundle/Controller/" type: annotation prefix: /

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/** * @Route("/project/{projectId}", requirements={"id" = "\d+"}, defaults={"id" = 1}) */public function getProjectAction($projectId){ return new Response("These are not the projects you're looking for");}

Configuration equals extendability:RestBundle provides us with CRUD annotations

RESTFUL API THE EASY WAYCRUD ROUTING ANNOTATIONS

convenientuse FOS\RestBundle\Controller\Annotations\Get;use FOS\RestBundle\Controller\Annotations\Put;

/** * Get all project * * @Get("/projects", name="todo_app_projects_read", options={"expose"=true}) * * @return JsonResponse */public function readProjectsActions() { /* ... */ }

/** * Update a project * * @Put("/project", name="todo_app_project_update", options={"expose"=true}) * * @return Response */public function updateProjectActions() { /* ... */ }

Using @GET and @POST routes in your docblocks will create a self-documenting API !

RESTFUL API THE EASY WAYRESTBUNDLE VIEW AND VIEWHANDLER

RESTFUL API THE EASY WAYRESTBUNDLE AND SERIALIZERBUNDLE ARE FRIENDS

namespace Rednose\TodoBundle\Common;

use JMS\Serializer\SerializationContext;use FOS\RestBundle\View\View;use Symfony\Bundle\FrameworkBundle\Controller\Controller as BaseController;

class Controller extends BaseController{ /** * Create a xml or json view based on the given entity * * @param string $format Valid options are: json|xml * @param array $groups The serializer context group(s) * @param mixed $entity * @return Response */ function getView($format, $groups, $entity) { $handler = $this->get('fos_rest.view_handler');

$view = new View(); $context = new SerializationContext(); $context->setGroups($groups);

$view->setSerializationContext($context); $view->setData($entity); $view->setFormat($format);

return $handler->handle($view); }}

RedNose

[email protected]

https://github.com/SavageTiger/Todo-App-Demo

QUESTIONS ?