Laravel eloquent model encoding json error - php

I am getting a encoding error with eloquent and i don't understand how to fix it
this is the the error:
Illuminate\Database\Eloquent\JsonEncodingException: Error encoding model [App\Models\UserIp] with ID [] to JSON: Recursion detected in file
this is the the function :
/**
* #param string $ip the user ipaddress
* #param string $status failed or succeeded ?
* #param string $type the type of action performed
*
* #throws \JsonException
*/
function logUserIp(string $ip, string $status, string $type)
{
$user = UserIp::where('ip', '=', $ip)->where('user_id', '=', auth()->user()->id)->first();
$position = Location::get($ip);
if (null == $user && false !== $position) {
$ip = new UserIp();
$ip->user_id = auth()->user()->id;
$ip->ip = $ip;
$ip->country = $position->countryName;
$ip->country_code = $position->countryCode;
$ip->status = $status;
$ip->type = $type;
$ip->device = json_encode([
'platform' => Agent::platform(),
'browser' => Agent::browser(),
'user_agent' => \request()->server('HTTP_USER_AGENT')
], JSON_THROW_ON_ERROR);
$ip->save();
}
}

There error is occuring from this line:
$ip->ip = $ip;
You have a parameter named $ip in your function signature (string $ip) but you're then overriding that value later with $ip = new UserIp(), so $ip is no longer a string but is now an instance of UserIp.
Later in your code you then assign the new $ip of type UserIp to your $ip->ip property. You're basically assigning the $ip instance of UserIp to the ip property on itself. I assume you actually mean to be the original string $ip.
Change the name of your new UserIp() variable and you should be good.

I saw you fixed your problem, but let me give you some small tips about your code, see my code and compare it with yours:
/**
* #param string $ip the user ipaddress
* #param string $status failed or succeeded ?
* #param string $type the type of action performed
*
* #throws \JsonException
*/
public function logUserIp(string $ip, string $status, string $type): void
{
$user = auth()->user();
if ($position = Location::get($ip) &&
!$user->userIp()->where('ip', $ip)->exists()
) {
$userIp = new UserIp();
$userIp->ip = $ip;
$userIp->country = $position->countryName;
$userIp->country_code = $position->countryCode;
$userIp->status = $status;
$userIp->type = $type;
$userIp->device = [
'platform' => Agent::platform(),
'browser' => Agent::browser(),
'user_agent' => request()->server('HTTP_USER_AGENT')
];
$user->associate($userIp);
$user->save();
}
}
See how I changed the code. Also, for $userIp->device to save without the need of json_encode, you will have to add this to your UserIp model:
/**
* The attributes that should be cast.
*
* #var array
*/
protected $casts = [
'device' => 'array',
];
So you can later consume $userIp->device as an array, no need to do json_decode($userIp->device).
Also, related to $user->associate, read this.

Related

yii2, how correctly cache data

Now i have code data like this:
my const
const CacheUserByUid = 'CacheUserByUid_';
const CacheUserByUsername = 'CacheUserByUsername_';
const CacheUserById = 'CacheUserByUsername_';
Get user data bu uid
/**
* Get user by uid , return user data for user profile
*
* #param $uid
* #return mixed
*/
public function getUserByUid($uid)
{
$result = Yii::$app->cache->getOrSet(self::CacheUserByUid . $uid, function () use ($uid) {
$result = self::find()
->select([
'id',
'username',
'email',
'city',
'country',
'name',
'avatar',
'about',
'uid',
])
->where(['uid' => trim($uid)])
->one();
if (!empty($result)) {
$result->id = (string)$result->id;
}
return $result;
});
return $result;
}
get user data by PK
/**
* #param $userId
* #return mixed
*/
public function getUserById($userId)
{
$user = Yii::$app->cache->getOrSet(self::CacheUserById . $userId, function () use ($userId) {
return self::findOne($userId);
});
return $user;
}
Get user by username
/**
* Get user by username. Return only for user front info
*
* #param $username
* #return array|\yii\db\ActiveRecord|null
*/
public function getUserByUsername($username)
{
$result = Yii::$app->cache->getOrSet(self::CacheUserByUsername . $username, function () use ($username) {
$result = self::find()->select([
'user.id',
'user.city',
'user.country',
'user.name',
'user.avatar',
'user.about',
'user.username'
])
->where(['username' => $username])
->one();
if (!empty($result)) {
$result->id = (string)$result->id;
}
});
return $result;
}
I cached this data. And where user was update i used:
/**
* #param $data
* #param $userId
* #return bool
* #throws \yii\db\Exception
*/
public function updateUser($data, $userId)
{
$user = $this->getUserById($userId);
if (!empty($user)) {
foreach ($data as $key => $name) {
if ($this->hasAttribute($key)) {
$user->$key = $name;
}
}
$user->updatedAt = time();
if ($user->save()) {
//чистимо кеш
FileCache::clearCacheByKey(self::CacheUserByUid . $user->uid);
FileCache::clearCacheByKey(self::CacheUserByUsername . $user->username);
FileCache::clearCacheByKey(self::CacheUserById . $user->id);
return true;
}
}
return false;
}
method clearCacheByKey
/**
* #param $key
*/
public static function clearCacheByKey($key)
{
if (Yii::$app->cache->exists($key)) {
Yii::$app->cache->delete($key);
}
}
Am I good at using a single-user cache that caches these requests in different keys? I don't see any other way out
Is it ok to cache user data in FileCache?
maybe it would be better to use something else for this?
In your case, such simple queries don't need to be cached explicitly. Yii already has a query cache and your requests definitely should be already stored in the cache. The key for data in cache would be a combination of your SQL's md5 with some connection metadata.
Just ensure that everything is configured correctly.
Also if you need to update cached data on some changes, make sure that you're making queries with the best for your case cache dependency. It can purge cached results by some auto condition or you can do it manually from your code(by using TagDependency)
What about FileCache it depends on traffic to your app and current infrastructure. Sometimes there is nothing criminal to store cache in files and you're always can switch to something like Redis/Memcache when your app grow big enough

php/symfony: retrieving an attribute in the url for POST/PUT api

For a datatable I use in a page (webix datatable), I have to use a REST API.
My url is for example: http://localhost:8000/trial/1
In this page to make the api call I use the following:
save: "rest->{{ path('api_i_post') }}",
url: "rest->{{ path('erp_interventionapi_get', { trialid: trial.id })
With the GET method, I retrieve for a trial (/trial/1), many interventions which are loaded from a database and filled in the datatable.
With this datatable, I'm able to "add a new row". It uses the POST method (save: "rest->{{ path('api_i_post') }}")
When I add a new row, I'd like to be able to get the field trial_id filled in automatically, depending from where I add a new row in the datatable (for /trial/1, trial_id = 1) but I don't know how to retrieve this attribute (or the trial object id), in a POST and a PUT.
My postAction:
/**
* #Rest\Post("/api_i/", name="api_i_post")
*/
public function postAction(Request $request)
{
$data = new Intervention;
$id = $request->get('id');
$action = $request->get('action');
$daadala = $request->get('daadala');
$date = $request->get('date');
$week = $request->get('week');
$infopm = $request->get('info_pm');
$comment = $request->get('comment');
$location = $request->get('location');
$trial = $request->get('trialid');
$data->setAction($action);
$data->setDaadala($daadala);
$data->setDate($date);
$data->setWeek($week);
$data->setWho($infopm);
$data->setInfoPm($comment);
$data->setComment($location);
$data->setTrial($trial);
$em = $this->getDoctrine()->getManager();
$em->persist($data);
$em->flush();
$lastid = $data->getId();
$response=array("id" => $id, "status" => "success", "newid" => $lastid);
return new JsonResponse($response);
$view = View::create(array("newid" => $lastid, "id" => $id, "status" => "success"));
return $this->handleView($view);
}
And my putAction
/**
* #Rest\Put("/api_i/{id}")
*/
public function putAction(Request $request)
{
$data = new Intervention;
$id = $request->get('id');
$action = $request->get('action');
$daadala = $request->get('daadala');
$date = $request->get('date');
$week = $request->get('week');
$infopm = $request->get('info_pm');
$comment = $request->get('comment');
$location = $request->get('location');
$sn = $this->getDoctrine()->getManager();
$intervention = $this->getDoctrine()->getRepository('ErpBundle:Sponsor')->find($id);
if (empty($intervention)) {
return new View("Sponsor not found", Response::HTTP_NOT_FOUND);
}
$intervention->setAction($action);
$intervention->setDaadala($daadala);
$intervention->setDate($date);
$intervention->setWeek($week);
$intervention->setWho($infopm);
$intervention->setInfoPm($comment);
$intervention->setComment($location);
$sn->flush();
$response=array("id" => $id, "status" => "success");
return new JsonResponse($response);
}
Can you help me with this issue?
Thank you very much
Update of my code after the replys:
I have update this in my twig template:
save: "rest->{{ path('api_i_post', { trialid: trial.id }) }}",
If I look in the profiler of the ajax request, I see it is here:
Key Value
trialid "1"
But I still don't figure how to get it in my post request (the trial_id is still null right now)
I've tried the following:
/**
* #Rest\Post("/api_i/", name="api_i_post")
* #Rest\RequestParam(name="trialid")
*
* #param ParamFetcher $paramFetcher
* #param Request $request
*/
public function postAction(Request $request, ParamFetcher $paramFetcher)
{
$data = new Intervention;
$id = $request->get('id');
$action = $request->get('action');
$daadala = $request->get('daadala');
$date = $request->get('date');
$week = $request->get('week');
$infopm = $request->get('info_pm');
$comment = $request->get('comment');
$location = $request->get('location');
$trial = $paramFetcher->get('trialid');
$data->setAction($action);
$data->setDaadala($daadala);
$data->setDate($date);
$data->setWeek($week);
$data->setWho($infopm);
$data->setInfoPm($comment);
$data->setComment($location);
$data->setTrial($trial);
$em = $this->getDoctrine()->getManager();
$em->persist($data);
$em->flush();
$lastid = $data->getId();
$response=array("id" => $id, "status" => "success", "newid" => $lastid);
return new JsonResponse($response);
$view = View::create(array("newid" => $lastid, "id" => $id, "status" => "success"));
return $this->handleView($view);
}
I guess you are using the FosRestBundle, if so, you can use annotations to retrieve your url parameters :
/**
* #Rest\Put("/api_i/{id}", requirements={"id" = "\d+"})
*/
public function putAction($id)
{
// you now have access to $id
...
}
If you want to allow additionnal parameters for your route but not in the uri, you can use RequestParam with annotations :
/**
* #Rest\Put("/my-route/{id}", requirements={"id" = "\d+"})
*
* #Rest\RequestParam(name="param1")
* #Rest\RequestParam(name="param2")
*
* #param ParamFetcher $paramFetcher
* #param int $id
*/
public function putAction(ParamFetcher $paramFetcher, $id)
{
$param1 = $paramFetcher->get('param1');
....
}
Be sure to check the fosRestBundle documentation to see everything you can do (such as typing the params, making them mandatory or not, etc...)
To get post value you need to do this inside your post action:
public function postAction(Request $request)
{
$postData = $request->request->all();
Then you have an array of value like:
$id = $postData['id'];
For the PUT you need this:
public function putAction(int $id, Request $request)
{
$putData = json_decode($request->getContent(), true);
And then to treieve a value like this:
$id = $putData['id'];

Laravel testing with JWT-Auth

I'm trying to test my api that's made with JWT_auth: https://github.com/tymondesigns/jwt-auth
class UpdateTest extends TestCase
{
use DatabaseTransactions;
public $token;
public function signIn($data = ['email'=>'mail#gmail.com', 'password'=>'secret'])
{
$this->post('api/login', $data);
$content = json_decode($this->response->getContent());
$this->assertObjectHasAttribute('token', $content);
$this->token = $content->token;
return $this;
}
/** #test */
public function a_user_updates_his_account()
{
factory(User::class)->create([
'name' => 'Test',
'last_name' => 'Test',
'email' => 'mail#gmail.com',
'mobile' => '062348383',
'function' => 'ceo',
'about' => 'About me.....',
'corporation_id' => 1
]);
$user = User::first();
$user->active = 2;
$user->save();
$this->signIn();
$url = '/api/user/' . $user->slug . '?token=' . $this->token;
$result = $this->json('GET', $url);
dd($result);
}
}
Result is always:
The token could not be parsed from the request
How do I get this t work!?
Source (https://github.com/tymondesigns/jwt-auth/issues/206)
One way to test your API in this situation is to bypass the actual token verification, but still log your user in (if you need to identify the user). Here is a snippet of the helper method we used in our recent API-based application.
/**
* Simulate call api
*
* #param string $endpoint
* #param array $params
* #param string $asUser
*
* #return mixed
*/
protected function callApi($endpoint, $params = [], $asUser = 'user#example.org')
{
$endpoint = starts_with($endpoint, '/')
? $endpoint
: '/' . $endpoint;
$headers = [];
if (!is_null($asUser)) {
$token = auth()->guard('api')
->login(\Models\User::whereEmail($asUser)->first());
$headers['Authorization'] = 'Bearer ' . $token;
}
return $this->json(
'POST',
'http://api.dev/' . $endpoint,
$params,
$headers
);
}
And is used like this:
$this->callApi('orders/list', [
'type' => 'customers'
])
->seeStatusOk()
Basically, there is not really a way for now. The fake request that is created during testing and is passed to Laravel to handle, somehow drops the token data.
It has alredy been reported in an issue (https://github.com/tymondesigns/jwt-auth/issues/852) but as far as I know, there is no solution yet.

How to collect tweets/retweets using GNIP API in php

Since twitter has switched to streaming his API, we have to collect the data by ourselves.
How we can do it using GNIP API in php?
Answering this, I’ve just wanted to ensure that I’ve done everything right and maybe to improve my TwitterGnipClient class with your help.
But if there are no answers, let it be FAQ style question.
Main methods see below:
/**
* Add rules to PowerTrack stream’s ruleset.
* #example ['id' => 'url', ...]
* #param array $data
*/
public function addRules(array $data)
{
$rules = [];
foreach ($data as $id => $url) {
$rules[] = $this->buildRuleForUrl($url, $id);
}
$this->httpClient->post($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'json' => ['rules' => $rules]
]);
}
/**
* Retrieves all existing rules for a stream.
* #return \Generator
*/
public function getRules()
{
$response = $this->httpClient->get($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()]
]);
$batchStr = '';
$body = $response->getBody();
while (!$body->eof()) {
$batchStr .= $body->read(1024);
}
$batchArray = explode(PHP_EOL, $batchStr);
unset($batchStr);
foreach ($batchArray as $itemJson) {
yield $this->unpackJson($itemJson);
}
}
/**
* Removes the specified rules from the stream.
* #param $data
*/
public function deleteRules($data)
{
$rules = [];
foreach ($data as $id => $url) {
$rules[] = $this->buildRuleForUrl($url, $id);
}
$this->httpClient->delete($this->getRulesUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'json' => ['rules' => array_values($rules)]
]);
}
/**
* Open stream through which the social data will be delivered.
* #return \Generator
*/
public function listenStream()
{
$response = $this->httpClient->get($this->getStreamUrl(), [
'auth' => [$this->getUser(), $this->getPassword()],
'stream' => true
]);
$batchStr = '';
$body = $response->getBody();
while (!$body->eof()) {
$batchStr .= $body->read(1024);
$batchArray = explode(PHP_EOL, $batchStr);
// leave the last piece of response as it can be incomplete
$batchStr = array_pop($batchArray);
foreach ($batchArray as $itemJson) {
yield $this->processBroadcastItem($this->unpackJson($itemJson));
}
}
$body->close();
}
/**
* Process broadcast item data
* #param $data
* #return array
*/
protected function processBroadcastItem($data)
{
if (is_array($data)) {
$url = str_replace('url_contains:', '', $data['gnip']['matching_rules'][0]['value']);
switch ($data['verb']) {
// Occurs when a user posts a new Tweet.
case 'post':
return $this->getMappedResponse($url, 'tweet', 1);
break;
// Occurs when a user Retweets another user's Tweet
case 'share':
return $this->getMappedResponse($url, 'retweet', $data['retweetCount']);
break;
}
}
return [];
}
All class I shared as Gist - here
P.S. If you see an evident issue - comment it please.

Laravel - input not passing over through unit test

I'm receiving the below error when running my unit tests. Seems that it doesn't like passing in the Input::get to the constructor, however when running the script within the browser the action works fine so I know it's not the controller code. If I take out any of the 'task_update' code the test passes with just the find even with the Input - so not sure why it accepts the Input for one method.
ErrorException: Argument 1 passed to Illuminate\Database\Eloquent\Model::__construct() must be of the type array, null given, called
My Controller is:
public function store()
{
$task_update = new TaskUpdate(Input::get('tasks_updates'));
$task = $this->task->find(Input::get('tasks_updates')['task_id']);
$output = $task->taskUpdate()->save($task_update);
if (!!$output->id) {
return Redirect::route('tasks.show', $output->task_id)
->with('flash_task_update', 'Task has been updated');
}
}
And the test is - I'm setting the input for task_updates array but just isn't being picked up:
Input::replace(['tasks_updates' => array('description' => 'Hello')]);
$mockClass = $this->mock;
$mockClass->task_id = 1;
$this->mock->shouldReceive('save')
->once()
->andReturn($mockClass);
$response = $this->call('POST', 'tasksUpdates');
$this->assertRedirectedToRoute('tasks.show', 1);
$this->assertSessionHas('flash_task_update');
I believe the "call" function is blowing away the work done by Input::replace.
the call function can actually take a $parameters parameter which should fix your problem.
if you look in \Illuminate\Foundation\Testing\TestCase#call, you'll see the function:
/**
* Call the given URI and return the Response.
*
* #param string $method
* #param string $uri
* #param array $parameters
* #param array $files
* #param array $server
* #param string $content
* #param bool $changeHistory
* #return \Illuminate\Http\Response
*/
public function call()
{
call_user_func_array(array($this->client, 'request'), func_get_args());
return $this->client->getResponse();
}
If you do:
$response = $this->call('POST', 'tasksUpdates', array('your data here'));
I think it should work.
I do prefer to do both Input::replace($input) and $this->call('POST', 'path', $input).
Example AuthControllerTest.php:
public function testStoreSuccess()
{
$input = array(
'email' => 'email#gmail.com',
'password' => 'password',
'remember' => true
);
// Input::replace($input) can be used for testing any method which
// directly gets the parameters from Input class
Input::replace($input);
// Here the Auth::attempt gets the parameters from Input class
Auth::shouldReceive('attempt')
->with(
array(
'email' => Input::get('email'),
'password' => Input::get('password')
),
Input::get('remember'))
->once()
->andReturn(true);
// guarantee we have passed $input data via call this route
$response = $this->call('POST', 'api/v1/login', $input);
$content = $response->getContent();
$data = json_decode($response->getContent());
//... assertions
}

Categories