I'm working on a project where the request and response payloads are all namespaced. For example:
{
'SourceMachine':{
'Host':'some value',
'Description':'some value',
'UserName':'some value',
'Password':'some value'
}
}
To be able to do get() and set() on the individual fields, I overrode the parse method in my "source_model" like so:
parse : function(response, xhr) {
return response.SourceMachine;
}
That's all well and good. But the issue is this: When I want to do a POST or PUT operation, I have to get the Host, Description, UserName and Password attributes into the SourceMachine namespace. Basically what I've been doing is copying the model attributes to a temporary object, clearing out the model, then saving as follows:
var tempAttributes = this.model.attributes;
this.model.clear();
this.model.save({SourceMachine: tempAttributes});
This works, but it screams of KLUGE! There's got to be a better way of working with namespaced data. Thanks!
Update
I've now created a require.js module for a base model that I'll be using for all the models in my app, since they all rely on namespaced data:
define(function() {
return Backbone.Model.extend({
/**
* Adding namespace checking at the constructor level as opposed to
* initialize so that subclasses needn't invoke the upstream initialize.
* #param attributes
* #param options
*/
constructor : function(attributes, options) {
//Need to account for when a model is instantiated with
//no attributes. In this case, we have to take namespace from
//attributes.
this._namespace = options.namespace || attributes.namespace;
//invoke the default constructor so the model goes through
//its normal Backbone setup and initialize() is invoked as normal.
Backbone.Model.apply(this, arguments);
},
/**
* This parse override checks to see if a namespace was provided, and if so,
* it will strip it out and return response[this._namespace
* #param response
* #param xhr
* #return {*}
*/
parse : function(response, xhr) {
//If a namespace is defined you have to make sure that
//it exists in the response; otherwise, an error will be
//thrown.
return (this._namespace && response[this._namespace]) ? response[this._namespace]
: response;
},
/**
* In overriding toJSON, we check to see if a namespace was defined. If so,
* then create a namespace node and assign the attributes to it. Otherwise,
* just call the "super" toJSON.
* #return {Object}
*/
toJSON : function() {
var respObj = {};
var attr = Backbone.Model.prototype.toJSON.apply(this);
if (this._namespace) {
respObj[this._namespace] = attr;
} else {
respObj = attr;
}
return respObj;
}
})
});
Create and update operations on models call toJSON to produce the data for the server:
toJSON model.toJSON()
Return a copy of the model's attributes for JSON stringification. This can be used for persistence, serialization, or for augmentation before being handed off to a view.
You could provide your own toJSON:
toJSON: function() {
return { SourceMachine: _(this.attributes).clone() };
}
That should make your server happy. Unfortunately, toJSON is also commonly used to provide data for your templates and you probably don't want the SourceMachine noise in there; if so, then add another method to prepare data for your templates:
// Or whatever you want to call it...
valuesForTemplate: function() {
return _(this.attributes).clone();
}
That's what the standard toJSON method does internally.
Related
I'm trying to setup FOS Rest Bundle on symfony 4.2.3. and I'm following this tutorial:
https://www.thinktocode.com/2018/03/26/symfony-4-rest-api-part-1-fosrestbundle/
Installed and configured the bundle:
//annotations.yaml
rest_controller:
resource: ../../src/Controller/Rest/
type: annotation
prefix: /api
//fos_rest.yaml
fos_rest:
view:
view_response_listener: true
format_listener:
rules:
- { path: ^/api, prefer_extension: true, fallback_format: json, priorities: [ json ] }
and when I call postAction() of my ArticleController object, which looks like:
class ArticleController extends FOSRestController
{
/**
* Creates an Article resource
* #Rest\Post("/articles")
* #param Request $request
* #return View
*/
public function postArticle(Request $request): View
{
$entityManager = $this->getDoctrine()->getManager();
$article = new Article();
$article->setTitle($request->get('title'));
$article->setContent($request->get('content'));
$articleCategory = $entityManager->getReference(ArticleCategory::class, $request->get('article_category'));
$article->setArticleCategory($articleCategory);
$article->setPublished($request->get('published'));
$entityManager->persist($article);
$entityManager->flush();
// In case our POST was a success we need to return a 201 HTTP CREATED response
return View::create($article, Response::HTTP_CREATED);
}
}
I get the error message (testing with Postman):
The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned an object of type FOS\RestBundle\View\View.
Why is that?! It's defined that postArticle() method should return a View object and I'm doing that.
One notice: my class is extending FOSRestController class, which is deprecated. I also tried extending AbstractFOSRestController, but with the same result.
So, for some reason my fos_rest.yaml file was not in packages dir, where it would be global, but in test sub-dir of packages. When I moved it where it belongs this started working.
I'm creating a SPA backed by Symfony and ApiPlatform so I want to always load my main route despite the real path of the URL.
I want something like this:
/**
* {#inheritdoc}
*/
class DefaultController extends Controller
{
/**
* #Route("/*", name="homepage")
*
* #return Response
*/
public function indexAction(): Response
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig');
}
}
In my intentions, also if the URL is something like /path/to/the/spa/page I want to anyway load the DefaultController::indexAction()route.
How to do this? (obviously the provided example doesn't work).
Ok, I've found the solution after an "illumination".
I remembered that there is the possibility to rewrite all URL adding or removing the trailing slash
Reading that article I saw this:
class RedirectingController extends Controller
{
/**
* #Route("/{url}", name="remove_trailing_slash",
* requirements={"url" = ".*\/$"})
*/
public function removeTrailingSlash(Request $request)
{
// ...
}
}
So, to intercept all URL despite the path, my DefaultController::indexAction() becomes this:
class DefaultController extends Controller
{
/**
* #Route("/{url}",requirements={"url"=".*"}, name="homepage")
*
* #return Response
*/
public function indexAction(): Response
{
// replace this example code with whatever you need
return $this->render('default/index.html.twig');
}
}
Now all URL are all handled by DefaultController::indexAction() despite the URL path.
I would recommend you to use symfony's event system instead.
Subscribe to either kernel.request or kernel.router events.
In case of kernel.request you have to overtake the Symfony\Component\HttpKernel\EventListener\RouterListener::onKernelRequest()
which priotiry is 32 (use 33 at least).
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class SpaSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 33],
];
}
public function onKernelRequest(RequestEvent $event)
{
$request = $event->getRequest();
if (!$request->isXmlHttpRequest()) {
$html = $this->twig->render('spa.html.twig', [
'uri' => $request->getUri(),
]);
$response = new Response($html, Response::HTTP_OK);
$event->setResponse($response);
}
}
}
In case of kernel.router use priority 1 at least.
You can use the php bin/console debug:event-dispatcher command to find out which listeners are registered for events and their priorities.
I'm building a new Ng 1 project using Breeze. I need to add client-side properties to some entities. I added a constructor, but it does not get called. I added a post initializer, it is not called either.
// a convention can self-register as the default
breeze.NamingConvention.camelCase.setAsDefault();
// create a manager to execute queries
service.manager = new breeze.EntityManager("/api/pts");
// access to the manager's store to add custom client-side properties ..
service.metastore = service.manager.metadataStore;
// entities with client-side properties ..
service.metastore.registerEntityTypeCtor('PtsWine__Pts_BreezeModel', PtsWine, ptsWineInitializer);
var PtsWine = function () {
debugger;
this.uiDayUid = 0;
}
var ptsWineInitializer = function (pw) {
debugger;
pw.uiDayUid = 0;
}
var query = new service.EntityQuery().from("Wines");
return service.manager.executeQuery(query)
.then(function (d) {
debugger;
/// ??? examining d.results shows 40 PtsWine__Pts_BreezeModel entities but
/// constructor and initializer above were never invoked .. ???
})
.fail(function (response) {
toastr.error("Server Error: failed to retrieve listing.")
});
The initialization sequence does not seem correct to me. At the time registerEntityTypeCtor is executed PtsWine and ptsWineInitializer are undefined. Remember javascript hoisting concept? https://developer.mozilla.org/en-US/docs/Glossary/Hoisting
You need to define the variable before calling registerEntityTypeCtor.
I'm wondering if there is a way to either pass additional parameters to the constructor (preferred) or retrieve the Request object to check the headers from within the Form constructor so that when I do a getForm in a controller, the form will be customized depending on how it is called?
I'm working on integrating AngularJs bindings and model tags into my form elements but I will need to modify how the submit button works whenever a form is called from Ajax vs being pulled into a Zend template via the framework.
Thus I would like to throw conditional parameters around where the submit button is added to the form, but I need to know if the rendered form is being viewed in zend or is being sent via an ajax call. I can detect the ajax call in the controller by looking at the request headers with isXmlHttpRequest(), but I'm not sure how to let the form know what the controller saw when it's retrieving the form with $this->getForm()
You can inject any options you like using a factory class.
use MyModule\Form\MyForm;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\FactoryInterface;
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $formElementManager)
{
$serviceManager = $formElementManager->getServiceLocator();
$request = $serviceManager->get('Request');
// I would recommend assigning the data
// from the request to the options array
$options = [
'is_ajax' => $request->isXmlHttpRequest(),
];
// Although you could also pass in the request instance into the form
return new MyForm('my_form', $options, $request);
}
}
If you inject the request you will need modify MyForm::__construct.
namespace MyModule\Form;
use Zend\Form\Form;
use Zend\Http\Request as HttpRequest;
class MyForm extends Form
{
protected $request;
public function __construct($name, $options, HttpRequest $request)
{
$this->request = $request;
parent::__construct($name, $options);
}
}
Update your module.config.php to use the factory
return [
'form_elements' => [
'factories' => [
'MyModule\Form\MyForm' => 'MyModule\Form\MyFormFactory'
]
]
]
Then ensure you request the form from the service manager (in a controller factory)
$myForm = $serviceManager->get('FormElementManager')->get('MyModule\Form\MyForm');
My AbstractForm with helper functions (I just added the getRequest() to the bottom). Of course in a wider scale application I'd probably add error checking to make sure these were not called from the constructor (when the service manager would not yet be available)
namespace Application\Form;
use Zend\Form\Form as ZendForm;
use Zend\Http\Request as HttpRequest;
use Zend\Stdlib\RequestInterface as Request;
use Zend\Form\FormElementManager as ZendFormElementManager;
use Zend\ServiceManager\ServiceLocatorAwareInterface as ZendServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface as ZendServiceLocatorInterface;
use Doctrine\ORM\EntityManager as DoctrineEntityManager
class AbstractForm extends ZendForm implements ZendServiceLocatorAwareInterface {
/**
* #var Request
*/
protected $request;
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #var ZendFormElementManager $service_manager
*/
protected static $service_manager;
/**
* #var DoctrineEntityManager $entity_manager
*/
protected $entity_manager;
/**
* #var ZendServiceLocatorInterface $service_locator_interface
*/
protected $service_locator_interface;
public function __construct($name = null)
{
parent::__construct($name);
}
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #param ZendFormElementManager $serviceLocator
*/
public function setServiceLocator(FormElementManager $serviceLocator)
{
self::$service_manager = $serviceLocator;
}
/**
* in form context this turns out to be Zend\Form\FormElementManager
*
* #return ZendFormElementManager
*/
public function getServiceLocator()
{
return self::$service_manager;
}
/**
* wrapper for getServiceLocator
* #return ZendFormElementManager
*/
protected function getFormElementManager() {
return $this->getServiceLocator();
}
/**
* this returns an actual service aware interface
*
* #return ZendServiceLocatorInterface
*/
protected function getServiceManager() {
if(!($this->service_locator_interface instanceof ZendServiceLocatorInterface)) {
$this->service_locator_interface = $this->getFormElementManager()->getServiceLocator();
}
return $this->service_locator_interface;
}
/**
* #return DoctrineEntityManager
*/
protected function getEntityManager() {
if(!($this->entity_manager instanceof \DoctrineEntityManager)) {
$this->entity_manager = $this->getServiceLocator()->getServiceLocator()->get('Doctrine\ORM\EntityManager');
}
return $this->entity_manager;
}
/**
* Get request object
*
* #return Request
*/
public function getRequest()
{
if (!$this->request) {
$this->request = $this->getServiceManager()->get('Request');
}
return $this->request;
}
}
I have these "methods" in my angular service that use restangular to get remote data where the respose is this:
{
"1105":{"title":"","field_nazione":{"und":[{"value":null,"format":null,"safe_value":""}]},"field_redazionale":{"und":[{"value":null}]}},
"1110":{"title":"","field_nazione":{"und":[{"value":null,"format":null,"safe_value":""}]},"field_redazionale":{"und":[{"value":null}]}}
};
function restfulService(ipCookie,Restangular) {
return {
//Setta la directory di partenza del service rest. In questo modo non devo sempre definirlo
restfulBase : function() {
return Restangular.oneUrl('rest','http://MYREMOTEHOST/rest');
},
getAllCity : function () {
return this.restfulBase().get('cities', {'all':1}, {}, {'X-CSRF-Token': tokenVar});
},
....
};
Why when I call getAllCity() the url is :
http://MYDOMAIN/rest?0=c&1=i&2=t&3=i&4=e&5=s
?
If I use this :
Restangular.oneUrl('rest','http://MYDOMAIN/rest/cities').get({all : 1});
I have no problems.
I have tried changing my app to set Restangular.setBaseUrl() in .config() method and then changing my service to use Restangular.all('cities').get() but I have an error about "strings".
If I use getAll() I have an error about "getLists() want array and not objects".
So: which is the correct way to use Restangular ? I have read online documentation and tutorial, but I have not understand how to retrieve elements in the right way. And to "post".
Thanks and sorry for this stupid question.
I believe the problem is that you set your base as "one" type of resource. The "one" method is not for setting the base urls. The way I set the base url is like this:
angular.module('app')
.config(['RestangularProvider', function (RestangularProvider) {
// Set the base URL
RestangularProvider.setBaseUrl('http://MYREMOTEHOST/rest');
}]);
And then I access each resource, depending on its type with "one" or "all", like so:
function restfulService(ipCookie,Restangular) {
return {
getAllCity : function () {
return Restangular.all('cities').getList();
},
....
};
You can also set the default params in the config phase, or per request at runtime.
The "getLists() want array and not objects" error you get when you use "all" is because it accepts only arrays as a response, therefore you need to intercept the data before and parse it in an array. In your case, you are getting an object, so you can parse it like this:
// this happens at runtime, either in your implementation code or in a more global config file, like:
Restangular.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
// If the what is set, than just grab it and return it
if (data.hasOwnProperty(what)) {
return data[what];
} else {
// Otherwise make it an array
var arr = [];
for (var key in data) {
arr.push(data[key]);
}
return arr;
});
Hope this helps.