working with symfony2 models in the front end
TRANSCRIPT
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?
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);}
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 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); }}