I'm making a JSON REST API using Symfony 5.2. One of the API calls returns a date, however I noticed the date is always serialized as an empty array ([]). Here is an extract from the response I get:
{"count":1,"data":[{"id": 6, "published_from":[],"published_until":[]}]}
The other fields like id are correct, but the dates aren't. In the database, the fields are populated with dates.
I tried running the query with the default services.yaml with the same result.
Here is the field declaration in the entity:
/**
* #ORM\Column(type="datetime")
* #Groups({"base", "updatable"})
*/
private $publishedFrom;
Finally, still edited for brevety, here is relevant business logic code (eg: in Controller):
$qb = $this->createQueryBuilder('p')
->andWhere('p.store = :store')
->setParameter('store', $store_id);
$paginator = new Paginator($qb, false);
$entity = ['count' => count($paginator), 'data' => $paginator);
return $this->json($entity, context: ['groups' => ['base']]);
It looks like the Symfony\Component\Serializer\Normalizer\DateTimeNormalizer isn't used by default, when I think it should be.
I'm fairly sure it's only a matter of configuration, but I can't find any clue.
After some debugging, I've seen that both dates are replaced by empty arrays at line 203 of AbstractObjectNormalizer:
$data = $this->updateData($data, $attribute, $this->serializer->normalize($attributeValue, $format, $this->createChildContext($context, $attribute, $format)), $class, $format, $context);
Thank you for any input.
EDIT: See my answer for more information on this bug.
Alright, I found the answer. Even after reusing the default services.yaml and running bin/console c:c, my custom Denormalizer was still loaded somehow. It does not override the supportsNormalization method. In the AbstractNormalizer stub, this resolves to true.
The solution for me was to override it: In my denormalizers I added this to prevent them from being used for normalization:
public function supportsNormalization($data, string $format = null)
{
return false;
}
I'll leave this issue up in case anyone encounters this very specific issue.
EDIT: I realized after re-enabling my services.yaml, that the snake_case converter re-enables this bug. I guess this is for the same reason. I have not found a workaround yet.
Github issue: https://github.com/symfony/symfony/issues/40818
The problem also can be with normalizer's order(priority).
You can set it with:
#services.yaml
get_set_method_normalizer:
class: Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer
tags:
- { name: serializer.normalizer, priority: -991 }
And check it:
php bin/console debug:container --tag=serializer.normalizer
Related
As part of my ongoing efforts to simplify the legacy codebase for a CodeIgniter3 application, I'm currently running into a problem. In short, I've dealt with an error statement earlier stating:
can't use method return value in write context
which might ring a bell for some readers. Nonetheless, I haven't seen this error since but I suspect that something is still going wrong. In short, I'm trying to push an associative array into another associative, multidimensional array which is the result of a method that returns a reference.
I've set up a system to easily alter the contents of a JSON, which is returned by reference through this method:
/**
* Function : items
* Target : Retrieves a reference to the items, decoded
*
* #author : Angev
* #since : 2.0
* #version : 1.0
*
* #return Referenced link to index 'items'.
*/
public function & items()
{
$list = (is_array($this->data)) ? $this->data : json_decode($this->data, true);
return $list['items'];
}
In short, the method $this->items() is part of a Model named 'CheckList'. The Checklist corresponds with a data-table in my database, which includes a 'data' column that represents the data belonging to a certain checklist: the JSON I'm trying to alter and which you can see being returned in this method. Another way of seeing the output of $this->items() is that it should return a reference to $this->data['items'].
This goes all and well, I've used this method many times during development - as a shorthand to accessing ['list'] - and it always returns exactly what I need it to return: a multidimensional array filled with unique indexes (strings) that contain the data belonging to each item of the checklist.
The problem however, arises in a method named update_checklist() in particular the following section:
$this->items()[$uid] = [
'parent_id' => $parent['id'],
... ,
];
I'd expect the method to add an index to the array returned by $this->items(), but it doesn't.
I'm not quite sure what goes wrong in this context, since I have earlier seen the error message written at the top of this question, but haven't seen it since.
However, no index is added to the array and whenever I do an immediate var_dump($this->items()) afterwards. It just shows the state of the array as it was before the execution of update_checklist().
In search of an answer, I've also tried wrapping the callback in parentheses, but to no avail:
( $this->items() )[$uid] = . . .
To temporary fix the problem, I've resorted to a more direct alteration of the ['items'] array by doing the following:
$this->data = json_decode($this->data, true);
$this->data['items'][$uid] = [
'parent_id' => $parent['id'],
... ,
];
Nonetheless, even though the code above works, I'm left wondering what the flaw is in my logic concerning the method reference return of $this->items() and why I cannot use this method when pushing into the referenced array.
How can I write the required changes to make $this->items()[] function as intended? Or I'd be interested in more clarity into the theory behind this structure and why it can't work.
As always, once you start formulating a question, you stumble upon the flaws in your logic. I've read over this question with a colleague and while discussing the solution just magically presented itself. I'll include the answer for future reference to anyone having the same problem.
/**
* Function : items
* Target : Retrieves a reference to the items, decoded
*
* #author : Angev
* #since : 2.0
* #version : 1.0
*
* #return Referenced link to index 'items'.
*/
public function & items()
{
$list = (is_array($this->data)) ? $this->data : json_decode($this->data, true);
return $list['items'];
}
The problem lies withing the items() method. This method surely returns a reference, but the reference is made to the preliminary variable $list, which in turn has no direct reference to $this->data. So instead of refering to $this->data['items'], the method returns a reference to $list, which is essentially a copy of $this->data, no no real reference.
To fix the problem, the following code was used:
public function & items()
{
if(!is_array($this->data) ) $this->data = json_decode($this->data, true);
return $this->data['items'];
}
As expected, the method now returns a reference to the actual data object.
So in short, what I've learned is that if you let a method return a reference, you need to make sure that whatever the method returns is actually a reference instead of a copy of the data you're trying to reference to.
I'll leave this question open for now to allow others to share any knowledge of insights in this matter.
EDIT:
I outputted the array and #apokryfos had mentioned something about resources not being able to be serialized.
Here is how some debug output looks: (Removed some information that is more sensitive)
Stream in Timestamp
It is my timestamp causing the issue. If I do
unset($user["timestamp"]);
Then almost everyone's solution works. So, the real reason was the resources was in there for my timestamp. How do I stop or fix that? I tried to do
public $timestamps = false;
This did not have any changes.
I have read through the documentation and a few tutorials. Sadly, I can't seem to find any documentation on what is returned and what functions are available for use when using Eloquent. Maybe I am just missing it.
However, I am using this code in my controller.
public function find($userName){
$user = UserSecurity::where('userName', $userName)->get();
//dd($user);
return Response()->json(['data' => $user], 200);
}
This is my router code.
$router->get('/UserSecurity/find/{userName}', ['uses'=>'UserSecurityController#find']);
I know that it is pulling the correct data from the database as if I uncomment the dd($user) I can see it on screen. However, if I try to send a response through Response()->json(..) it fails with this screen.
Image of Exception
I know I am probably using Response() incorrectly, but I have actually tried a number of different ways. Some just show empty responses and some crash similarly.
I have tried removing get() which from what I have found just returns nothing as there are no results. I have tried Response($user) and it was empty. I have tried return Response()->json($user); with the same type unsupported error.
Thanks for any help.
EDIT:
Changing a few code for testing. I changed to this
public function find($userName){
$user = UserSecurity::where('userName', $userName)->get()->toJson();
$user = json_encode($user);
return Response($user);
}
This returns false . I am not sure where the boolean is coming from. The original dd($user) actually has the correct information from the DB, so I know it is doing the query correct.
I think you must add the following in the top of your controller
then this code will help you.
use Illuminate\Support\Facades\Response;
The error message Type is not supported is actually coming from PHP's JSON serializer. There are only a very few types that PHP cannot serialise and the one that seems to be causing the issue in your particular case is a stream resource.
In order to check what is actually serialized in your model you will need to call:
$user = UserSecurity::where('userName', $userName)->get();
$user->map->jsonSerialize()->dd();
jsonSerialize exists because all Laravel models implement the JsonSerializable interface.
This will dump a collection of arrays of what PHP will attempt to serialise as JSON. The contents of this array are recursively serialised.
The default implementation of JsonSerializable in Model will attempt to serialize all model attributes but first will attempt to cast and call all accessors on attributes. This may or may not cause issues. At any rate the solution here is to figure out why there's a resource being returned in the jsonSerialize method and figure out the best way to hide it. Normally you can hide attributes by using
protected $hidden = [ 'timestamp' ];
however from your question it seems that the answer may not be so straight forward so may need to dig deeper.
I think you must add the following in the top of your controller:
use Illuminate\Support\Facades\Response;
and the controller must be like:
public function find($userName){
$user = UserSecurity::where('userName', $userName)->get();
return Response::json($user, 200);
}
PLease try like this
return response()->json(['status' => true, 'data' => $user],200);
I am already using this in my code
Using where('...')->get() returns a collection which cannot be used in response()->json().
Try using:
public function find($userName){
$user = UserSecurity::where('userName', $userName)->first();
return response()->json(['data' => $user->toArray()], 200);
}
here is just a typo error, you must write response() in lowercase instead or Response() because Response is a class, while response() is a magic Laravel function which already instantiates the class Response, and that you can use to return a Response instance.
I'm doing a PHPUnit on my controller and I can't seem to mock the Request right.
Here's the Controller:
use Illuminate\Http\Request;
public function insert(Request $request)
{
// ... some codes here
if ($request->has('username')) {
$userEmail = $request->get('username');
} else if ($request->has('email')) {
$userEmail = $request->get('email');
}
// ... some codes here
}
Then on the unit test,
public function testIndex()
{
// ... some codes here
$requestParams = [
'username' => 'test',
'email' => 'test#test.com'
];
$request = $this->getMockBuilder('Illuminate\Http\Request')
->disableOriginalConstructor()
->setMethods(['getMethod', 'retrieveItem', 'getRealMethod', 'all', 'getInputSource', 'get', 'has'])
->getMock();
$request->expects($this->any())
->method('get')
->willReturn($requestParams);
$request->expects($this->any())
->method('has')
->willReturn($requestParams);
$request->expects($this->any())
->method('all')
->willReturn($requestParams);
// ... some codes here
}
The problem here is that when ever I var_dump($request->has('username'); it always return the $requestParams value in which is the whole array. I'm expecting that it should return true as the username key exists in the array.
Then when I delete the username key on the $requestParams, it should return false as it does not contain the username key on the array
Its not ideal to mock Requests, but sometimes you just want to do it anyway:
protected function createRequest(
$method,
$content,
$uri = '/test',
$server = ['CONTENT_TYPE' => 'application/json'],
$parameters = [],
$cookies = [],
$files = []
) {
$request = new \Illuminate\Http\Request;
return $request->createFromBase(
\Symfony\Component\HttpFoundation\Request::create(
$uri,
$method,
$parameters,
$cookies,
$files,
$server,
$content
)
);
}
As far as I can see and understand you're telling your unit test that when you call $request->has() on your request object that it should return the $requestParams array, not true or false, or anything else.
Unless you specifically check what is send with a method call your mock doesn't actually care what is send, it just cares that it was called.
You might want to explore creating an empty request and filling it with data if that is possible in your use case as that'll let you run your unit test with more ease and less issues. This won't work in all cases.
You could include what assertions you're making in your unit test so we can see more clearly what you're running into, but as it is. It returns exactly what you're telling it to return. Even if that's not what you actually want it to return.
Mocks are used to separate your Unit-Test from the rest of your system. As such you usually tend to only check if a specific method is called to see if your code actually exits to the class you mocked and if it has the expected data you'd send along. In some extreme cases you can want to mock the system you're actually testing, but this usually indicates that your code is too dependent on other classes or it's doing too much.
Another reason to use mocks is to satisfy Type Casting constraints in your method calls. In these cases you'll usually create an empty mocked object and fill it with some dummy data your code will accept or break on to test the code.
In your case it seems you want to check if your code actually works correctly and for this I'd suggest either not mocking the request, or making specific tests where you tell it to return true, or false (test for both cases)
So something along the lines of:
$request->expects($this->any())
->method('has')
->with('username')
->willReturn(true); // or false in your next test
Edit:
As you mentioned in the comment Below you ran into the issue that you're using the has method multiple times in your code and ran into issues.
The Questions I've linked to in my response comment go into greater detail but to sum it up, you can use an inline function or the at() method to deal with multiple cases.
With at() you can supply specific iterations of the code to hit only that bit of the test. It has been mentioned that this makes your tests rather brittle as any has added before the previous ones would break the test.
$request->expects($this->at(0))
->method('has')
->with('username')
->willReturn('returnValue');
$request->expects($this->at(1))
->method('has')
->with('email')
->willReturn('otherReturnValue');
The inline function (callback) solution would allow you to customize your test to allow multiple cases and to return data as required. Unfortunately I'm not too familiar with this concept as I haven't used it myself before. I suggest reading the PHPUnit docs for more information about this.
In the end I'd still suggest not mocking the request and instead making an empty request that you'll fill with the data you want to check. Laravel comes with some impressive methods that'll let you manually fill the request with a lot of data you'd usually test against.
For example you can add data (post/get data) by using
request->add(['fieldname' => 'value'])
As a last few pointers I'd like to mention that it seems you use var_dump.
Laravel comes with two of it's own functions that are similar and quite useful in debugging.
You can use dd(); or dump();
dd(); dumps and stops the execution of code, while dump(); just outputs whatever you decide. so you could do dd($request); or dump($request); and see what the variables/class objects/etc holds. It'll even put it in a rather spiffy layout with some Javascript and such to allow you to see what's in it and such. Might want to check it out if you didn't knew it existed.
If you use request()->user() you can set user resolver. It allows you to return user you want. I had the same problem and solution for me was like this:
public function testSomething()
{
$user = User::factory()->create();
request()->setUserResolver(function() use ($user) {
return $user;
});
// Dumped result will be newly created $user
dd(request()->user());
}
A simpler answer than #Ian, if your situation is simpler:
Per https://stackoverflow.com/a/61903688/135114,
if
your function under test takes a $request argument, and
you don't need to do funky stuff to the Request—real route paths are good enough for you
... then you don't need to "mock" a Request (as in, mockery),
you can just create a Request and pass it, e.g.
public function test_myFunc_condition_expectedResult() {
...
$mockRequest = Request::create('/path/that/I_want', 'GET');
$this->assertTrue($myClass->myFuncThat($mockRequest));
}
I was running unit test on a FormRequest child class with Laravel Framework 9.3.0 and get this error:
Error : Call to a member function get() on null
/vendor/symfony/http-foundation/Request.php:676
# code failing
$customRequest->get('parameter');
As you can see in Request class, there are lot of public properties (source code):
public $attributes;
public $request;
public $query;
public $server;
public $files;
public $cookies;
public $headers;
...
This is the way i find to partially mock Request class, example below:
# test code
$this->customRequest = new CustomRequest();
$parameterBag = \Mockery::mock(ParameterBag::class);
$parameterBag->shouldReceive('get')
->with('parameter', \Mockery::any())
->andReturn(null) // anything
;
$this->customRequest->attributes = $parameterBag;
I made two bundles. I have created an action method in each of their respective controllers.
I am trying to get result of A in B.
I have attempted to include the content of the view of what A generates in B but my include ends up missing a variable therefore I retrieve the result from forwarding it.
I read the documentation and decided to use $variable=$this->forwad(MyControllerA) but my $variable doesn't get any result when I read the data in the view.
I was wondering as I found no similar issue if I am doing something wrong or if I should do this differently.
Code
public function getAAction()
{
$em = $this->getDoctrine()->getManager();
$list_A = $em->getRepository('ABundle:A_Entity')->findAll();
return $list_A;
}
public function getBAction()
{
$em = $this->getDoctrine()->getManager();
$list_B = $em->getRepository('PropertyBundle:B_Entity')->findAll();
$list_A = $this->forward('A_Bundle:A_Entity:getA');
return $this->render('#B/B/getB.html.twig', array('list_B' => $list_B, 'list_A' => $list_A));
}
It should be $variable=$this->forwad(MyBundle:MyControllerA:myAction) and not $variable=$this->forwad(MyControllerA)
http://symfony.com/doc/current/controller/forwarding.html
Show us the code so you can get better help
I found out i was wrong.
I dumped the variable as suggested by #Rendy and did way deeper research to try and understand more. it averred that forward works with response.
From there i tried manipulating the response i got but due to my lack of knowledge and despite the research i couldn't find how to retrieve the array i sent.
Therefore i converted my controller into service as follow in services.yml of my bundle
services:
bundle_a:
class: BundleA\Controller\AController
calls:
- [ "setContainer", [ "#service_container" ] ]
If you're wondering about the last line its because when calling the container is null. That line fixed it.
For more info you can look at Symfony: Why is method $this->get() not available in constructor?
Then i just called my method in the B Controller as follow
$list_a= $this->get('bundle_a')->getAAction();
Is there any open source (or example) code for Symfony2 which can filter certain model using multiple parameters? A good example of what I'm looking for can be seen on this Trulia web page.
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/0-500_price/wd,dw_amenities/sm_dogs_pets"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400-500_price/wd,dw_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/wd,dw_amenities"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400p_price/dw,cs_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/1p_beds/1p_baths/400p_price/dw,cs_amenities
Note how URL are build when clicking in the form, I guess is using one controller for all this routes, How is it done?.
I Don't think it will be redirecting all the possible routes to a specific controller, (shown below), maybe some sort of dynamic routing?
/**
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{mix_price}-{max_price}_price /{amenities_list}
* #Route("/for_rent/{state}/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{bath}_bath/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{mix_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{amenities_list}
* ........
*/
public function filterAction($state, $beds, $bath, $min_price, $max_price ....)
{
....
}
Thanks.
For simple queries (i.e. where you don't need to have a data range, such as min-max value), you can use the entity repository to find entities by the request parameters given. Assuming that your entity is Acme\FooBundle\Entity\Bar:
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeFooBundle:Bar');
$criteria = array(
'state' => $state,
'beds' => $beds,
// and so on...
);
$data = $repo->findBy($criteria);
When building the $criteria array, you'll probably want some logic so that you only sort by criteria that have been provided, instead of all possible values. $data will then contain all entities that match the criteria.
For more complex queries, you'll want to look into DQL (and perhaps a custom repository) for finer-grained control of the entities that you're pulling out.
To construct your routes, i'm sure you had a look at the Routing page of the documentation, but did you notice that you can put requirements on routes? This page explains how to do it with annotations.
As for the filtering, I suppose DQL would be ok, but you can also write straight up SQL with Doctrine, and map the results of your query to one or more entities. This is described here. It may be more flexible than DQL.
csg, your solution is good (with #Route("/search/{q}) if you only need to use routing in "one-way". But what if you will need to print some price filter links on page accessible by url:
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
In case of #Route("/search/{q} you will not be able to use route method url generate with params.
There is a great Bundle called LexikFormFilterBundle "lexik/form-filter-bundle": "~2.0" that helps you generate the complex DQL after the Filter form completed by the user.
I created a Bundle, that depends on it, that changes the types of a given FormType (like the one generated by SencioGeneratorBundle) So you can display the right FilterForm and then create the DQL after it (with Lexik).
You can install it with Composer, following this README.md
All it does is override the Doctrine Type Guesser, that suggests the required FormType for each Entity field, and replace the given Type by the proper LexikFormFilterType. For instance, replaces a simple NumberType by a filter_number which renders as two numbers, Max and Min interval boundaries.
private function createFilterForm($formType)
{
$adapter = $this->get('dd_form.form_adapter');
$form = $adapter->adaptForm(
$formType,
$this->generateUrl('document_search'),
array('fieldToRemove1', 'fieldToRemove2')
);
return $form;
}
Upon form Submit, you just give it to Lexik and run the generated query, as shown in my example.
public function searchAction(Request $request)
{
// $docType = new FormType/FQCN() could do too.
$docType = 'FormType/FQCN';
$filterForm = $this->createFilterForm($docType);
$filterForm->handleRequest($request);
$filterBuilder = $this->getDocRepo($docType)
->createQueryBuilder('e');
$this->get('lexik_form_filter.query_builder_updater')
->addFilterConditions($filterForm, $filterBuilder);
$entities = $filterBuilder->getQuery()->execute();
return array(
'entities' => $entities,
'filterForm' => $filterForm->createView(),
);
}