I am writing an api to get an object array by returning a paginated object. The problem is that accessing first page of the paginate object works fine but second and next pages just only return the first page contents. I just called the URLs like this:
/api/get?page=1
/api/get?page=2
Data going in:
{
"page_id":116,
"descending":true,
"rowsPerPage":10,
"page":1,
"search":null,
"sortBy":"name",
"tag_id":null,
"start_date":null,
"end_date":null,
"from_mobile":true
}
Below is my code:
function get_list(Request $req)
{
$orderby = "id";
$order = "DESC";
$flowdb = Flow::leftjoin("flow_tags", "flow.id", "=", "flow_tags.flow_id");
$flowdb->leftjoin("tags", "tags.id", "=", "flow_tags.tag_id");
$flowdb->leftjoin("flow_stocks", "flow.id", "=", "flow_stocks.flow_id");
$flowdb->select(DB::Raw("flow.id,flow.name,flow.used_count,flow.payload,GROUP_CONCAT(CONCAT('\"','[',tags.id,',', tags.name,']','\"')) as tags, count(flow_stocks.stock_id) as stock_count"));
$flowdb->where("flow.active", ">", 0);
$flowdb->where("flow.page_id", $req->page_id);
$flowdb->groupBy("flow.id");
if($req->from_mobile)
{
$flowdb->where('flow.from_mobile',true);
}
if ($req->sortBy) {
$orderby = $req->sortBy;
}
$order = $req->descending ? 'DESC' : 'ASC';
$flowdb->orderBy($orderby, $order);
$flowdb->orderBy("flow_tags.id", "ASC");
if ($req->rowsPerPage && $req->rowsPerPage !== -1) {
$flows = $flowdb->paginate($req->rowsPerPage);
} else {
$flows = $flowdb->paginate(999999999);
}
return $flows;
}
I also tried laravel manual paginator but the error didn't solve.
public function paginator(Request $request, $object, $perPage)
{
$currentPage = LengthAwarePaginator::resolveCurrentPage()??1;
$currentItems = array_slice($object, $perPage * ($currentPage - 1), $perPage);
$paginatedItems = new LengthAwarePaginator($currentItems, count($object), $perPage, $currentPage);
$paginatedItems->setPath($request->url());
return $paginatedItems;
}
Take a look at this method. The parameter $perPage is how many results you wish to show per page.
/**
* Paginate the given query.
*
* #param int|null $perPage
* #param array $columns
* #param string $pageName
* #param int|null $page
* #return \Illuminate\Contracts\Pagination\LengthAwarePaginator
*
* #throws \InvalidArgumentException
*/
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
$page = $page ?: Paginator::resolveCurrentPage($pageName);
$perPage = $perPage ?: $this->model->getPerPage();
$results = ($total = $this->toBase()->getCountForPagination())
? $this->forPage($page, $perPage)->get($columns)
: $this->model->newCollection();
return $this->paginator($results, $total, $perPage, $page, [
'path' => Paginator::resolveCurrentPath(),
'pageName' => $pageName,
]);
}
This means something is wrong with setting up the requirements for this condition:
$req->rowsPerPage && $req->rowsPerPage !== -1
Double check your queries by using dd($query)
I think it would be easier to retrieve the whole dataset as is, without setting condition flow.page_id, $req->page_id I think the culprit is somewhere there, but it's difficult to verify without having the data for it.
Finally have you tried calling to ->get() before $req->rowsPerPage && $req->rowsPerPage !== -1:
$flowdb->get();
not sure if it would help. But I've seen instances where it was required. Can't think of any examples though.
I managed to find out the solution. The problem is I accidentally put page = 1 in the request parameter that laravel paginate used this page value (in this case 1) to render results for all pages. I read some online articles about this and found out that laravel starts rendering from page 1 by default if the user didn't give specific page value in the request. So, I didn't really need that page parameter in this case.
In case any one having the same issue using the http client, suppose the url is as bellow:
$url = 'http://127.0.0.1:8002/api/trips?page=3';
just dd the response to see which path was actually requested to the server
dd(Http::get($url)->effectiveUri())
It look something like this:
GuzzleHttp\Psr7\Uri {#2021 ▼
-scheme: "http"
-userInfo: ""
-host: "127.0.0.1"
-port: 8002
-path: "/api/trips"
-query: "page=3"
-fragment: ""
}
there you can confirm in the query field that the page number is what you intended.
Related
How can I implement Laravel's cursor paginator without returning the id column in the collection?
The following query:
'users' => User::cursorPaginate(15)
returns the individual users like this:
{
"id": 1,
"uuid": "376bec76-9095-4510-a5ba-fea0f234c6cf",
"username": "alexanderhorner",
"password": "$2y$12$qMITOdMr2XdAq3EMKwc/WeB/db9IaQdkZ5egqY7CX5WpUwwHLKOLK"
}
Now, lets say I have an API. I want api/v1/user to return the same paginated results using cursorPaginate(), but I don't want to return the id column.
'users' => User::select('uuid', 'username')->cursorPaginate(15)
would return an error though, since cursorPaginate() needs the ID column or something similar:
InvalidArgumentException
Illegal operator and value combination.
What's the best way to fix this? Filter the collection returned by cursorPaginate()?
You can also check the cursorPaginate implementation (and signature) inside Builder.php.
There is a parameter columns. Check it out:
/**
* Get a paginator only supporting simple next and previous links.
*
* This is more efficient on larger data-sets, etc.
*
* #param int|null $perPage
* #param array $columns
* #param string $cursorName
* #param \Illuminate\Pagination\Cursor|string|null $cursor
* #return \Illuminate\Contracts\Pagination\CursorPaginator
*/
public function cursorPaginate($perPage = 15, $columns = ['*'], $cursorName = 'cursor', $cursor = null)
{
return $this->paginateUsingCursor($perPage, $columns, $cursorName, $cursor);
}
You can map the Users Collection of Models before return as response.
By doing the following you map the properties you want in a new variable called $users_to_be_returned and you return this in a property in your reponse.
You can do whatever with your structure of response with arrays, stdClass or even better DTO's.
$users = User::cursorPaginate(10);
$users_dtos = collect($users->items())->map(function ($user) {
$user_dto = new UserDTO(); // or even an stdClass object
$user_dto->username = $user->username;
$user_dto->password = $user->password;
return $user_dto;
})->toArray();
$response = new UserResponseDTO();
$response->users = $users_dtos;
$response->pagination_info = new PaginationInfoDTO();
$response->pagination_info->per_page = $users->perPage();
$response->pagination_info->path = $users->path();
$response->pagination_info->next_cursor = $users->nextCursor()->parameter('users.id');
$response->pagination_info->next_page_url = $users->nextPageUrl() ? $users->nextPageUrl() : null;
$response->pagination_info->previous_cursor = $users->previousCursor() ? $users->previousCursor()->parameter('users.id') : null;
$response->pagination_info->prev_page_url = $users->previousPageUrl() ? $users->previousPageUrl() : null;
return response()->json($response);
I wonder if Laravel have any helper to modify a collection.
What I need to do is to make a query with paginate() then check if the logged in users ID match the sender or receiver and based on that add a new value to the output:
$userId = Auth::guard('api')->user()->user_id;
$allMessages = Conversation::join('users as sender', 'conversations.sender_id', '=', 'sender.user_id')
->join('users as reciver', 'conversations.recipient_id', '=', 'reciver.user_id')
->where('sender_id',$userId)->orWhere('recipient_id',$userId)
->orderBy('last_updated', 'desc')
->select('subject','sender_id','recipient_id', 'sender_unread', 'recipient_unread', 'last_updated', 'reciver.username as receivername', 'sender.username as sendername')
->paginate(20);
Now I want to do something like:
if ($allMessages->sender_id == $userId) {
// add new value to output
newField = $allMessages->sendername
} else {
// add new value to output
newField = $allMessages->receivername
}
Then send the data with the new value added
return response()->json(['messages' => $allMessages], 200);
Is this possible?
You're better off using the Collection class's built-in functions for this. For example, the map function would be perfect.
https://laravel.com/docs/5.3/collections#method-map
$allMessages = $allMessages->map(function ($message, $key) use($userId) {
if ($message->sender_id == $userId) {
$message->display_name = $message->receivername;
} else {
$message->display_name = $message->sendername;
}
return $message;
});
Solved by adding:
foreach ($allMessages as $message) {
if ($message->sender_id == $userId) {
$message->display_name = $message->receivername;
} else {
$message->display_name = $message->sendername;
}
}
You can surely use the laravel's LengthAwarePaginator.
Along with total count of collection you also need to pass the slice of collection's data that needs to be displayed on each page.
$total_count = $allMessages->count();
$per_page = 2;
$current_page = request()->get('page') ?? 1;
$options = [
'path' => request()->url(),
'query' => request()->query(),
];
Suppose you want 2 results per page then calculate the offset first
$offset = ($current_page - 1) * $per_page;
Now slice the collection to get per page data
$per_page_data = $collection->slice($offset, $per_page);
$paginated_data = new LengthAwarePaginator($per_page_data, $total_count, $per_page, $current_page, $options);
$paginated_data will have only limited number of items declared by $per_page variable.
If you want next two slice of data then pass api_request?page="2" as your url.
As I don't know which Laravel version you're using, taking Laravel 5.2 let me give you a smarter way to deal with this (if I get your problem correctly).
You can use Laravel's LengthAwarePaginatior(API Docs).
Don't use paginate method when you are bulding your query, instead of that use simple get method to get simple collection.
$userId = Auth::guard('api')->user()->user_id;
$allMessages = Conversation::join('users as sender', 'conversations.sender_id', '=', 'sender.user_id')
->join('users as reciver', 'conversations.recipient_id', '=', 'reciver.user_id')
->where('sender_id',$userId)->orWhere('recipient_id',$userId)
->orderBy('last_updated', 'desc')
->select('subject','sender_id','recipient_id','sender_unread','recipient_unread','last_updated','reciver.username as receivername','sender.username as sendername')
->get();
Now you can populate extra items into that collection based on your certain conditions like this.
if ($allMessages->sender_id == $userId ) {
// add new value to collection
} else {
// add new value to collection
}
Now use LengthAwarePaginator, to convert that populated collection into a paginated collection.
$total_count = $allMessages->count();
$limit = 20;
$current_page = request()->get('page');
$options = [
'path' => request()->url(),
'query' => request()->query(),
];
$paginated_collection = new LengthAwarePaginator($allMessages, $total_count, $limit, $current_page, $options);
The variable $paginated_collection now can be used to be sent in response. Hope this helps you to deal with your problem.
So I decided to go from Laravel 4 to 5 which took me around 1-2 days because I barely knew how to do the transition. While doing the Upgrade for my app i came across a small problem with Json Pagination.
This code is what allows a PageQuery to be Paginated Via KnockoutJS
/**
* Builds paginate query with given parameters.
*
* #param array $params
* #param integer $page
* #param integer $perPage
*
* #return array
*/
public function buildPaginateQuery(array $params, $page = 1, $perPage = 15)
{
$query = $this->model;
$query = $this->appendParams($params, $query);
$count = (new Cache)->remember('count', '2000', function() use ($query){
return $query->count();
});
$totalPages = $count / $perPage;
$query = $query->skip($perPage * ($page - 1))->take($perPage);
$query = $query->order(isset($params['order']) && $params['order'] ? $params['order'] : null);
//$query = $query->cacheTags(array($this->model->table, 'pagination'))->remember(2000);
$query = (new Cache)->remember(array($this->model->table, 'pagination'), '2000', function() use ($query){
return $query;
});
return array('query' => $query, 'totalPages' => $totalPages, 'totalItems' => $count);
}
which eventually lead to this error in this screenshot
The Error directs to the code above and this code specifically
/**
* Get the full path for the given cache key.
*
* #param string $key
* #return string
*/
protected function path($key)
{
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
$path = $this->directory() . '/'.join('/', $parts).'/'.$hash;
//unset the tags so we use the base cache folder if no
//tags are passed with subsequent call to the same instance
//of this class
//$this->tags = array();
return $path;
}
Im using a custom Cache Driver called TaggedFile. This worked fine in L4 but came across errors because There were some files removed within the Cache Alias. Like the StoreInterface. Can I receive some help for this? If you need me to post anything I will.
More Stuff:
Before I used this to Register the taggedFile Driver in global.php:
Cache::extend('taggedFile', function($app)
{
return new Illuminate\Cache\Repository(new Lib\Extensions\TaggedFileCache);
});
I do not know where exactly to put this. Does anyone know the equivalent of that? I tried putting it in AppServiceProvider but an error came up saying:
Call to undefined method Illuminate\Support\Facades\Cache::extend()
This used to work in L4 so i decided to go into the vendor folder manually find what the problem was....
This only had: getFacadeAccessor (Which L4 also only had but extend worked)
So i decided to use getFacadeAccessor and it worked, but i don't know if that was the solution or not.
As you noticed you are passing an array as a $key value, the safest way would be to replace the code
$parts = array_slice(str_split($hash = md5($key), 2), 0, 2);
With
$parts = array_slice(str_split($hash = md5(json_encode($key)), 2), 0, 2);
NB: I am not sure what version of php you are running, but json_encode( ... ) is normally faster then serialize( ... )
I am checking the type of optional parameters in PHP like this:
/**
* Get players in the team using limit and
* offset.
*
*
* #param TeamInterface $participant
* #param int $limit
* #param int $offset
* #throws \InvalidArgumentException
* #return Players of a team
*/
public function getPlayers(TeamInterface $team, $limit = null, $offset = null)
{
if (func_num_args() === 2 && !is_int($limit) ){
throw new \InvalidArgumentException(sprintf('"Limit" should be of int type, "%s" given respectively.', gettype($limit)));
}
if (func_num_args() === 3 && (!is_int($limit) || !is_int($offset))){
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
//.....
}
This works but there are 2 main issues with this:
1/ If I need to check the type of 4/5 optional parameters for the same int type, the code become unnecessarily long. Any ideas how to make this piece of code more maintainable? (Maybe use only one if statement to check the same type of both $limit and $offset)
2/ getPlayers($team, 2, null) throws an exception. Is this ok knowing that the function can actually handle a null value here?
You could do a for loop with an array of args. Something like:
$args = func_get_args();
for ($i = 1; $i < 4; $i++) {
if ($args[$i] !== null and !is_int($args[$i])) {
throw ...
}
}
Of course, adjust the for conditions based on your number of arguments that need to be checked.
Or...
$args = func_get_args();
// skip first
array_shift($args);
foreach ($args as $arg) {
if ($arg !== null and !is_int($arg)) {
throw ...
}
}
For 1) I would check each variable individually and throw an exception for each:
if (!is_int($limit)){
//Throw
}
if (!is_int($offset))){
//Throw
}
This still requires an if statement for each variable but is a bit less verbose.
For 2) if null values are allowed you can change the check to be something like:
if ($offset && !is_int($offset))){
//Throw
}
Finally I wouldn't recommend checking func_num_args(). In your example code calling your function with too many arguments would bypass the validation.
PHP doesn't have type hints for scalars yet.
Redesign
When you start to take a lot of optional arguments in your function you develop code smells. Something is wrong, there is an object waiting to emerge.
Build all of your optional parameters as an Object and have a validate method on it.
I think you want a GameParameters object and have a validate method on it.
getPlayers($gameParameters) {
}
Move your validation of the parameters to that object where you can build it into each setter or have a comprehensive validate() function.
Combinatorial problem
As far as the explosion of checks goes I would build an array of errors and throw that if there are errors. This can be done with or without redesign.
if ($limit != null && !is_int($limit){
#add to the errors array
}
if ($offset != null && !is_int($offset){
#add to the errors array
}
if (errors) {
throw new \InvalidArgumentException(sprintf('"Limit" and "Offset" should be of int type, "%s" and "%s" given respectively.', gettype($limit), gettype($offset)));
}
Personally I prefer to have only one argument per function (unless the function is very simple), For example the function can take $request, and returns a tree of data $response. It makes it a bit easier to loop over and extend later:
function dostuff( $request ) {
$team = #$request['team'];
$limit = #$request['limit'];
$offset = #$request['offset'];
// ...
return $response;
}
Then for validation, you can write a set of rules at the top of the function like
// define validation rules
$rules = array( 'required' => array('team'),
'depends' => array('offset' => 'limit'),
'types' => array('offset' => 'int', 'limit' => 'int' ),
);
And centralize all your error checking in one call:
// can throw exception
argcheck( array( 'request' => $request, 'rules' => $rules ) );
This might need optimization, but the general approach helps contain bloat as you increase the complexity of the functions.
Use switch to code specific functions.
switch(gettype($limit)) {
case "integer":
//do other processing
break;
}
You cannot leave your code vulnerable like that. As for a safe solution to overcome the vulnerabilities. Create a safe list like this.
public function getPlayers(TeamInterface $team, $limit = null, $offset = null) {
$safelist = array("var1" => "TeamInterface", "var2" => "integer", "var3" => "integer");
$args = function_get_args();
$status = true;
foreach($args as $key => $var) {
if(gettype($var)!=$safelist["var".$key]) {
$status = false;
break;
}
}
if(!$status) break;
//...........
}
I'm basically formatting urls before sending my object to the view to loop through (with a foreach() on $submissions. The problem I'm having is that parse_url() takes a single index and not an entire array object.
I've got this method in my SubmissionsController:
public function newest() {
$submissions = $this->Submission->find('all', array(
'conditions' => array('Submission.approved' => '1'),
'order' => 'Submission.created DESC'
));
$this->set('submissions', $submissions);
$this->set('sourceShortUrl', AppController::shortSource($submissions));
}
In my AppController I've got this method which returns the formatted url:
protected function shortSource($source) {
return $sourceShortUrl = str_ireplace('www.', '', parse_url($source, PHP_URL_HOST));
}
This works for single entries, but parse_url can't take arrays, so is there a way in the controller to send the index of the object? E.g. $submissions['Submission']['source'] before I loop through it in the view?
My alternative was to do something like this in my shortSource($source) method:
if (is_array($source)) {
for ($i = 0; $i < count($source); $i++) {
return $sourceShortUrl = str_ireplace('www.', '', parse_url($source[$i]['Submission']['source'], PHP_URL_HOST));
}
}
But that's just returning the first (obviously). What is the best way to do this?
You're on the right track. Check for an array. If it's an array, call it recursively for each item in the array.
/**
* shortSource
*
* Returns an array of URLs with the www. removed from the front of the domain name.
*
* #param mixed $source Either a string or array
* #return mixed $sourceShortUrl An array of URLs or a single string
*/
protected function shortSource($source) {
if (is_array($source)) {
foreach ($source as $url) {
$sourceShortUrl[] = $this->shortSource($url);
}
} else {
$sourceShortUrl = str_ireplace('www.', '', parse_url($source, PHP_URL_HOST));
}
return $sourceShortUrl;
}
In this recursive function, it will parse a single string or an array of strings.
// in the view
if (is_array($sourceShortUrl)) {
foreach ($sourceShortUrl as $url) {
// view specific code for URL here
}
}