Does anyone knows how to add new data field to existing JsonResponse (Symfony\Component\HttpFoundation\JsonResponse) in after filter (onKernelResponse) without deleting old data?
For example, I have custom controller
<?php
class MessageController extends Controller
{
public function getAllMessagesAction(Request $request)
{
(...)
return new JsonResponse(array(
'count' => count($messages),
'total_count' => $allMessagesCount,
'messages' => $messages,
));
}
}
and some listener
<?php
class NotesListener
{
public function onKernelResponse(FilterResponseEvent $event)
{
(...)
$response = $event->getResponse();
if ($response instanceof JsonResponse) {
$response->setData(array('foo' => 'bar'));
}
}
}
Problem
The problem is that $response->setData in listener will override data passed in controller. Moreover, JsonResponse doesn't have method like addData(). Unfortunately there is not method getData(), so I can not get old data, modifying it, and set new data.
Does anyone know good solution?
Thanks in advance!
You can use getContent() and decode the the data, then modify it and set the data again.
Alternatively, you could change your controller to just return the data and create the JsonResponse in an event listener.
Related
I am creating a new API call for our project.
We have a table with different locales. Ex:
ID Code
1 fr_CA
2 en_CA
However, when we are calling the API to create Invoices, we do not want to send the id but the code.
Here's a sample of the object we are sending:
{
"locale_code": "fr_CA",
"billing_first_name": "David",
"billing_last_name": "Etc"
}
In our controller, we are modifying the locale_code to locale_id using a function with an extension of FormRequest:
// This function is our method in the controller
public function createInvoice(InvoiceCreateRequest $request)
{
$validated = $request->convertLocaleCodeToLocaleId()->validated();
}
// this function is part of ApiRequest which extend FormRequest
// InvoiceCreateRequest extend ApiRequest
// So it goes FormRequest -> ApiRequest -> InvoiceCreateRequest
public function convertLocaleCodeToLocaleId()
{
if(!$this->has('locale_code'))
return $this;
$localeCode = $this->input('locale_code');
if(empty($localeCode))
return $this['locale_id'] = NULL;
$locale = Locale::where(Locale::REFERENCE_COLUMN, $localeCode)->firstOrFail();
$this['locale_id'] = $locale['locale_id'];
return $this;
}
If we do a dump of $this->input('locale_id') inside the function, it return the proper ID (1). However, when it goes through validated();, it doesn't return locale_id even if it's part of the rules:
public function rules()
{
return [
'locale_id' => 'sometimes'
];
}
I also tried the function merge, add, set, etc and nothing work.
Any ideas?
The FormRequest will run before it ever gets to the controller. So trying to do this in the controller is not going to work.
The way you can do this is to use the prepareForValidation() method in the FormRequest class.
// InvoiceCreateRequest
protected function prepareForValidation()
{
// logic here
$this->merge([
'locale_id' => $localeId,
]);
}
I want to devide my controller to several service layer, validation layer, and logical layer.
But I got stuck when I want to send new variable to validation request, my schenario is, if there is a new sent it indicates for new data, and if new variable is not exists it indicates it's updating data.
Here is my Controller:
public function store(AlbumRequest $request, AlbumService $service)
{
$request->add(['new' => true])
try{
$service->store($request);
return redirect(route('admin.web.album.index'));
}catch(\Exception $err){
return back()->withInput()->with('error',$err->getMessage());
}
}
Here is my Request
class AlbumRequest extends FormRequest
{
public function rules()
{
dd($this->request->get('new')
}
}
I want to catch variable I have sent from Controller to Request. How to do that?
Thank you.
You can add new parameter in request from controller like that
$request->merge(array('new' => true));
before your request reaches your controller , it has to go through AlbumRequest class . so you have to merge that field in AlbumRequest class by using method prepareForValidation :
protected function prepareForValidation()
{
$this->merge([
'new' => true,
]);
}
add this method in your AlbumRequest class and see if it works
I am afraid you cannot do that because the incoming form request is validated before the controller method is called. Now if you want to know whether the request is for creating something new or updating something, you can do it by accessing the route parameters and method type
public function rules()
{
$rules = [
'something' => 'required',
];
if (in_array($this->method(), ['PUT', 'PATCH'])) {
//it is an edit request
//you can also access router parameter here, $this->route()->parameter('other thing');
$rules['somethingelse'] = [
'required',
];
}
return $rules;
}
You can enter the requested class as URL params.
class AlbumRequest extends FormRequest
{
public function rules()
{
dd(request()->get('new')
}
}
I have a data that I want to return in all of my controller methods and I thought that'd be cleaner to return from the controller's constructor; because currently it's just repetitive return code.
protected $data;
public function __construct() {
$this->data = "Test";
}
public function index() {
// Stuff
return view('test')->with([
'testData' => $this->data
// other view data
]);
}
public function store() {
// Stuff
return redirect()->back()->with([
'testData' => $this->data
// other view data
]);
}
This is just a pseudo example.
Yes, that's possible. It's done exactly in the same manner you showed.
However, I don't think that's the best way to do it. You might want to take a look into ViewComposers, which help provide a data set to a view (or multiple views) right after the controller but before the view is finally provided to the user.
You could just write a controller method to append the data property for you:
protected function view($name, $data = [])
{
return view($name, $data + $this->data);
}
public function index() {
...
return $this->view('view', ['other' => 'data']);
}
You can use ViewComposer which allow you to attach data to the view every time certain views is rendered
namespace App\ViewComposers;
class DataComposer
{
protected $data = ['1', '2', '3']; // This data is just for sample purpose
public function compose(View $view)
{
$view->with('data', $this->data);
}
}
And to register this composer with the list of all views on which the data must by attached add this code in the boot method off the AppServiceProvider
View::composer(
['view1', 'view2', 'view3', '....'],
'App\ViewComposers\DataComposer'
);
create a after middleware However, this middleware would perform its task after the request is handled by the application
Yes, it is very much possible with the code you have
[EDIT]
If you wish to remove ['testData' => $this->data] from all controller methods, then you can use view composers.
View composers are linked to one view. So for that view, if you have the same set of data to be passed all the time, use view composers!
I went through offical guide and found a way to envelop JSON data like this.
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
This works perfect when I have a collection and then I have a response like this.
{
products:....
}
But what I want to do is that i have a envelope for single data. For example if I do products/10 GET request to get.
{
product:
}
Hope somebody figured it out.
Single Data Envelope is not supported by \yii\rest\Serializer. At least until Yii 2.0.6 only collections get enveloped in order to add _links and _meta data objects to the response.
To envelope single data resource objects you'll need to override ActiveController's default view action within your Controller :
public function actions()
{
$actions = parent::actions();
unset($actions['view']);
return $actions;
}
public function actionView($id)
{
$model = Product::findOne($id);
return ['product' => $model];
}
Old, but I just bumped into here with the same problem.
And found a better (I think) solution: create your own serializer class extending \yii\rest\Serializer:
class Serializer extends \yii\rest\Serializer
{
public $itemEnvelope;
public function serializeModel($model)
{
$data = parent::serializeModel($model);
if($this->itemEnvelope)return [$this->itemEnvelope=>$data];
return $data;
}
}
And then use it like this:
public $serializer = [
'class' => '[your-namespace]\Serializer',
'collectionEnvelope' => 'list',
'itemEnvelope' => 'item'
];
An AJAX request to one of my controller actions currently returns the full page HTML.
I only want it to return the HTML (.phtml contents) for that particular action.
The following code poorly solves the problem by manually disabling the layout for the particular action:
$viewModel = new ViewModel();
$viewModel->setTerminal(true);
return $viewModel;
How can I make my application automatically disable the layout when an AJAX request is detected? Do I need to write a custom strategy for this? Any advice on how to do this is much appreciated.
Additionally, I've tried the following code in my app Module.php - it is detecting AJAX correctly but the setTerminal() is not disabling the layout.
public function onBootstrap(EventInterface $e)
{
$application = $e->getApplication();
$application->getEventManager()->attach('route', array($this, 'setLayout'), 100);
$this->setApplication($application);
$this->initPhpSettings($e);
$this->initSession($e);
$this->initTranslator($e);
$this->initAppDi($e);
}
public function setLayout(EventInterface $e)
{
$request = $e->getRequest();
$server = $request->getServer();
if ($request->isXmlHttpRequest()) {
$view_model = $e->getViewModel();
$view_model->setTerminal(true);
}
}
Thoughts?
Indeed the best thing would be to write another Strategy. There is a JsonStrategy which can auto-detect the accept header to automatically return Json-Format, but as with Ajax-Calls for fullpages, there it's good that it doesn't automatically do things, because you MAY want to get a full page. Above mentioned solution you mentioned would be the quick way to go.
When going for full speed, you'd only have one additional line. It's a best practice to always return fully qualified ViewModels from within your controller. Like:
public function indexAction()
{
$request = $this->getRequest();
$viewModel = new ViewModel();
$viewModel->setTemplate('module/controller/action');
$viewModel->setTerminal($request->isXmlHttpRequest());
return $viewModel->setVariables(array(
//list of vars
));
}
I think the problem is that you're calling setTerminal() on the view model $e->getViewModel() that is responsible for rendering the layout, not the action. You'll have to create a new view model, call setTerminal(true), and return it. I use a dedicated ajax controller so there's no need of determining whether the action is ajax or not:
use Zend\View\Model\ViewModel;
use Zend\Mvc\MvcEvent;
use Zend\Mvc\Controller\AbstractActionController;
class AjaxController extends AbstractActionController
{
protected $viewModel;
public function onDispatch(MvcEvent $mvcEvent)
{
$this->viewModel = new ViewModel; // Don't use $mvcEvent->getViewModel()!
$this->viewModel->setTemplate('ajax/response');
$this->viewModel->setTerminal(true); // Layout won't be rendered
return parent::onDispatch($mvcEvent);
}
public function someAjaxAction()
{
$this->viewModel->setVariable('response', 'success');
return $this->viewModel;
}
}
and in ajax/response.phtml simply the following:
<?= $this->response ?>
Here's the best solution (in my humble opinion). I've spent almost two days to figure it out. No one on the Internet posted about it so far I think.
public function onBootstrap(MvcEvent $e)
{
$eventManager= $e->getApplication()->getEventManager();
// The next two lines are from the Zend Skeleton Application found on git
$moduleRouteListener = new ModuleRouteListener();
$moduleRouteListener->attach($eventManager);
// Hybrid view for ajax calls (disable layout for xmlHttpRequests)
$eventManager->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController', MvcEvent::EVENT_DISPATCH, function(MvcEvent $event){
/**
* #var Request $request
*/
$request = $event->getRequest();
$viewModel = $event->getResult();
if($request->isXmlHttpRequest()) {
$viewModel->setTerminal(true);
}
return $viewModel;
}, -95);
}
I'm still not satisfied though. I would create a plugin as a listener and configure it via configuration file instead of onBootstrap method. But I'll let this for the next time =P
I replied to this question and seems it maybe similar - Access ViewModel variables on dispatch event
Attach an event callback to the dispatch event trigger. Once this event triggers it should allow you to obtain the result of the action method by calling $e->getResult(). In the case of an action returning a ViewModel it should allow you to do the setTerminal() modification.
aimfeld solution works for me, but in case some of you experiment issues with the location of the template, try to specify the module:
$this->viewModel->setTemplate('application/ajax/response');
The best is to use JsonModel which returns nice json and disable layout&view for you.
public function ajaxCallAction()
{
return new JsonModel(
[
'success' => true
]
);
}
I had this problem before and here is a quikc trick to solved that.
First of all, create an empty layout in your layout folder module/YourModule/view/layout/empty.phtml
You should only echo the view content in this layout this way <?php echo $this->content; ?>
Now In your Module.php set the controller layout to layout/empty for ajax request
namespace YourModule;
use Zend\Mvc\MvcEvent;
class Module {
public function onBootstrap(MvcEvent $e) {
$sharedEvents = $e->getApplication()->getEventManager()->getSharedManager();
$sharedEvents->attach(__NAMESPACE__, 'dispatch', function($e) {
if ($e->getRequest()->isXmlHttpRequest()) {
$controller = $e->getTarget();
$controller->layout('layout/empty');
}
});
}
}
public function myAjaxAction()
{
....
// View - stuff that you returning usually in a case of non-ajax requests
View->setTerminal(true);
return View;
}