laravel - pagination on collection - php

Im trying to work on eloquent collection. Got User model and Article model with relation one to many. One User can have many Articles. One Article belongs to one User.
This is how I retrieve articles written by a specifield user in a controller:
public function articles(User $user) {
if(empty($user)) $user = Auth::user();
$articles = $user->articles->paginate(10); //here comes collection
// passing results to view
return view('articles.index', compact('articles'))
->with('i', (request()->input('page', 1) - 1) * 10);
}
I found many solutions for paginating results that are a collection. The on I like the most is here: https://stackoverflow.com/a/56142421/14330487
My code is same. Using laravel 9, php 8.1.3 right now on a new project. I've created Collection::macro in AppServiceProvider boot() method.
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Collection;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
// builtin bootstrap views for pagination
Paginator::useBootstrapFive();
/**
* Paginate collection
*
* #param int $perPage
* #param int $total
* #param int $page
* #param string $pageName
* #return array
*/
Collection::macro('paginate', function ($perPage, $total = null, $page = null, $pageName = 'page') {
$page = $page ?: LengthAwarePaginator::resolveCurrentPage($pageName);
return new LengthAwarePaginator(
$this->forPage($page, $perPage),
$total ?: $this->count(),
$perPage,
$page,
[
'path' => LengthAwarePaginator::resolveCurrentPath(),
'pageName' => $pageName,
]
);
});
}
}
Dont know why my VSC shows me that forPage() and count() methods are undefined...
I dont get any errors running my app. But also cant see any results.
Routing, controller and view is fine, when Im doing it another way it shows results, but the way I get it is a crap. Using Collection::macro is far more gentle.
I have in my AppServiceProvider.php file lines like use Illuminate\Support\Collection and LengthAwarePaginator.
Really dont know why it doesnt work. Maybe its laravel 9 fault? Solution I found looks like made for laravel 8.
Thanks for help.

I've done that another way, without AppServiceProvider, but just by creating a Helper class. Solution found here: https://sam-ngu.medium.com/laravel-how-to-paginate-collection-8cb4b281bc55
My first attepmt did not work cause of method articles argument default value declaration. Should looks like:
public function articles(User $user = null) {}
and not known definition of forPage() and count() method are just display errors from VSC.
Both solutions works fine.
Just considering now which attempt is better.

Use this function
firstItem()
lastItem()
total()
links()
Example
#Controller
$orders = DB::table('orders')->paginate(15);
#View
$orders->firstItem();
$orders->lastItem();
$orders->total();
$orders->links();

Related

Laravel filter data after with closure

I have one quite simple question, Imagine I have Orders model and now I am writing something like that :
Order::where('status', 1)->with('orderer')->get();
Ok. It's simple and returns something like that:
{
id: 1,
price: 200,
status: 1,
income: 21,
orderer_id: 4,
orderer: {
//some orderer fields
}
}
now I don't want to get the whole object, I want to remove income, orderer_id and status properties from data. if I write something like that : get(["id", "price"]) I end up without orderer object (get(["id", "price", "orderer"]) doesn't work too), I couldn't make it work even using select(), so what is the solution? Also I don't want to hide it from everyone, for example admin should know income but user shouldn't, so $hidden field will not work.
You can add select() but make sure select does not take array but comma separated arguments :
$orders = Order::where('status', 1)->with('orderer');
if($user->role == 'admin'){
$orders->select('id','income','status','price');
}
else{
$orders->select('id','status','price');
}
$orders = $orders->get();
Above will first check the current logged in user's role and accordingly will select the columns required.
https://scotch.io/bar-talk/hiding-fields-when-querying-laravel-eloquent-models
In your Order Eloquent model:
protected $hidden = array('hide_this_field', 'and_that_field');
Edit: You want to filter based on role like Admin or User, next time please write that down in your question as well. Well a solution for that is to capture the DB query result, and walk that array, then unset properties of the model if the user is not an admin.
Edit2: I also see a discussion here which might help. Some user suggested using middle ware:
https://laracasts.com/discuss/channels/laravel/hide-eloquent-fields-based-on-user-role-or-any-model
If you are looking for a built in Laravel way to handle this, you could use API Resources: https://laravel.com/docs/5.7/eloquent-resources
php atrisan make:resource OrderResource
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$current_role = $request->user()->role; //or however you determine admin etc
$out = [
'id' => $this->id,
'price' => $this->price,
'orderer'=> $this->orderer,
];
if($current_role == 'admin'){
$out['income'] = $this->income;
$out['status'] = $this->status;
}
return $out;
}
}
In your Controller action
return OrderResource::collection(Order::where('status', 1)->with('orderer')->get());
If you want something a little more robust, consider https://github.com/spatie/laravel-fractal

Laravel Manual Pagination Error

I have a issue related to Laravel5 Manual Pagination.
I need to process an array of query builder result and some other arrays and make a pagination for that.
controller.
public function searchCategory($id){
$arr = DB::table('business_businesscatagories')->lists('fk_business_id');
return Paginator::make($arr, count($arr), 2);
}
Namespaces..
use Illuminate\Pagination\LengthAwarePaginator as Paginator;
But I got the error like below..
FatalErrorException in IndexController.php line 122:
Call to undefined method Illuminate\Pagination\LengthAwarePaginator::make()
How can I solve this issue....
I examined Illuminate\Pagination\LengthAwarePaginator and was able to confirm that it doesn't have make method.
However you can achieve this by using its constructor method.
__construct(mixed $items, int $perPage, int|null $currentPage = null, array $options = array())
Since you already have the correct namespace specified:
use Illuminate\Pagination\LengthAwarePaginator as Paginator;
You could simply do this:
$paginator = new Paginator($items, $total, $per_page);

How to manually create a new empty Eloquent Collection in Laravel 4

How do we create a new Eloquent Collection in Laravel 4, without using Query Builder?
There is a newCollection() method which can be overridden by that doesn't really do job because that is only being used when we are querying a set result.
I was thinking of building an empty Collection, then fill it with Eloquent objects. The reason I'm not using array is because I like Eloquent Collections methods such as contains.
If there are other alternatives, I would love to hear them out.
It's not really Eloquent, to add an Eloquent model to your collection you have some options:
In Laravel 5 you can benefit from a helper
$c = collect(new Post);
or
$c = collect();
$c->add(new Post);
OLD Laravel 4 ANSWER
$c = new \Illuminate\Database\Eloquent\Collection;
And then you can
$c->add(new Post);
Or you could use make:
$c = Collection::make(new Post);
As of Laravel 5. I use the global function collect()
$collection = collect([]); // initialize an empty array [] inside to start empty collection
this syntax is very clean and you can also add offsets if you don't want the numeric index, like so:
$collection->offsetSet('foo', $foo_data); // similar to add function but with
$collection->offsetSet('bar', $bar_data); // an assigned index
I've actually found that using newCollection() is more future proof....
Example:
$collection = (new Post)->newCollection();
That way, if you decide to create your own collection class for your model (like I have done several times) at a later stage, it's much easier to refactor your code, as you just override the newCollection() function in your model
Laravel >= 5.5
This may not be related to the original question, but since it's one of the first link in google search, i find this helpful for those like me, who are looking for how to create empty collection.
If you want to manually create a new empty collection, you can use the collect helper method like this:
$new_empty_collection = collect();
You can find this helper in Illuminate\Support\helpers.php
snippet:
if (! function_exists('collect')) {
/**
* Create a collection from the given value.
*
* #param mixed $value
* #return \Illuminate\Support\Collection
*/
function collect($value = null)
{
return new Collection($value);
}
}
Just to add on to the accepted answer, you can also create an alias in config/app.php
'aliases' => array(
...
'Collection' => Illuminate\Database\Eloquent\Collection::class,
Then you simply need to do
$c = new Collection;
In Laravel 5 and Laravel 6 you can resolve the Illuminate\Database\Eloquent\Collection class out of the service container and then add models into it.
$eloquentCollection = resolve(Illuminate\Database\Eloquent\Collection::class);
// or app(Illuminate\Database\Eloquent\Collection::class). Whatever you prefer, app() and resolve() do the same thing.
$eloquentCollection->push(User::first());
For more information about understanding resolving objects out of the service container in laravel take a look here:
https://laravel.com/docs/5.7/container#resolving
I am using this way :
$coll = new Collection();
$coll->name = 'name';
$coll->value = 'value';
$coll->description = 'description';
and using it as normal Collection
dd($coll->name);
It is better to use the Injection Pattern and after $this->collection->make([]) than new Collection
use Illuminate\Support\Collection;
...
// Inside of a clase.
...
public function __construct(Collection $collection){
$this->collection = $collection;
}
public function getResults(){
...
$results = $this->collection->make([]);
...
}
What worked for me was to name the use namespace and instantiate it directly:
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
# Usage
$this->latest_posts = new EloquentCollection();
Allowed me to merge two data subsets of eloquent collection results, this maintains the relationships - a regular collection (collect()) loses relationship and probably some more metadata.
$limit = 5;
$this->latest_posts = new EloquentCollection();
$pinned_posts = PinnedPostReference::where('category', $category)->get();
if($pinned_posts->count() > 0) {
foreach($pinned_posts as $ppost) {
$this->latest_posts->push($ppost->post);
}
}
# Another Eloquent result set ($regular_posts)
foreach($regular_posts as $regular_post) {
$this->latest_posts->push($regular_post);
}

Laravel Custom Model Methods

Whenever I add additional logic to Eloquent models, I end up having to make it a static method (i.e. less than ideal) in order to call it from the model's facade. I've tried searching a lot on how to do this the proper way and pretty much all results talk about creating methods that return portions of a Query Builder interface. I'm trying to figure out how to add methods that can return anything and be called using the model's facade.
For example, lets say I have a model called Car and want to get them all:
$cars = Car::all();
Great, except for now, let's say I want to sort the result into a multidimensional array by make so my result may look like this:
$cars = array(
'Ford' => array(
'F-150' => '...',
'Escape' => '...',
),
'Honda' => array(
'Accord' => '...',
'Civic' => '...',
),
);
Taking that theoretical example, I am tempted to create a method that can be called like:
$cars = Car::getAllSortedByMake();
For a moment, lets forget the terrible method name and the fact that it is tightly coupled to the data structure. If I make a method like this in the model:
public function getAllSortedByMake()
{
// Process and return resulting array
return array('...');
}
And finally call it in my controller, I will get this Exception thrown:
Non-static method Car::getAllSortedByMake() should not be called statically, assuming $this from incompatible context
TL;DR: How can I add custom functionality that makes sense to be in the model without making it a static method and call it using the model's facade?
Edit:
This is a theoretical example. Perhaps a rephrase of the question would make more sense. Why are certain non-static methods such as all() or which() available on the facade of an Eloquent model, but not additional methods added into the model? This means that the __call magic method is being used, but how can I make it recognize my own functions in the model?
Probably a better example over the "sorting" is if I needed to run an calculation or algorithm on a piece of data:
$validSPG = Chemical::isValidSpecificGravity(-1.43);
To me, it makes sense for something like that to be in the model as it is domain specific.
My question is at more of a fundamental level such as why is all()
accessible via the facade?
If you look at the Laravel Core - all() is actually a static function
public static function all($columns = array('*'))
You have two options:
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
or
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Both will allow you to do
Car::getAllSortedByMake();
Actually you can extend Eloquent Builder and put custom methods there.
Steps to extend builder :
1.Create custom builder
<?php
namespace App;
class CustomBuilder extends \Illuminate\Database\Eloquent\Builder
{
public function test()
{
$this->where(['id' => 1]);
return $this;
}
}
2.Add this method to your base model :
public function newEloquentBuilder($query)
{
return new CustomBuilder($query);
}
3.Run query with methods inside your custom builder :
User::where('first_name', 'like', 'a')
->test()
->get();
for above code generated mysql query will be :
select * from `users` where `first_name` like ? and (`id` = ?) and `users`.`deleted_at` is null
PS:
First Laurence example is code more suitable for you repository not for model, but also you can't pipe more methods with this approach :
public static function getAllSortedByMake()
{
return Car::where('....')->get();
}
Second Laurence example is event worst.
public function scopeGetAllSortedByMake($query)
{
return $query->where('...')->get();
}
Many people suggest using scopes for extend laravel builder but that is actually bad solution because scopes are isolated by eloquent builder and you won't get the same query with same commands inside vs outside scope. I proposed PR for change whether scopes should be isolated but Taylor ignored me.
More explanation :
For example if you have scopes like this one :
public function scopeWhereTest($builder, $column, $operator = null, $value = null, $boolean = 'and')
{
$builder->where($column, $operator, $value, $boolean);
}
and two eloquent queries :
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->where('first_name', 'like', 'b');
})->get();
vs
User::where(function($query){
$query->where('first_name', 'like', 'a');
$query->whereTest('first_name', 'like', 'b');
})->get();
Generated queries would be :
select * from `users` where (`first_name` like ? and `first_name` like ?) and `users`.`deleted_at` is null
vs
select * from `users` where (`first_name` like ? and (`id` = ?)) and `users`.`deleted_at` is null
on first sight queries look the same but there are not. For this simple query maybe it does not matter but for complicated queries it does, so please don't use scopes for extending builder :)
for better dynamic code, rather than using Model class name "Car",
just use "static" or "self"
public static function getAllSortedByMake()
{
//to return "Illuminate\Database\Query\Builder" class object you can add another where as you want
return static::where('...');
//or return already as collection object
return static::where('...')->get();
}
Laravel model custom methods -> best way is using traits
Step #1: Create a trait
Step #2: Add the trait to model
Step #3: Use the method
User::first()->confirmEmailNow()
app/Model/User.php
use App\Traits\EmailConfirmation;
class User extends Authenticatable
{
use EmailConfirmation;
//...
}
app/Traits/EmailConfirmation.php
<?php
namespace App\Traits;
trait EmailConfirmation
{
/**
* Set email_verified_at to now and save.
*
*/
public function confirmEmailNow()
{
$this->email_verified_at = now();
$this->save();
return $this;
}
}

Laravel 4 - Using Eloquent Models in a custom library class

I made a library class that I am using for some common functions not provided by Laravel. It's been loaded into /config/app.php under the 'aliases' array, so that shouldn't be the problem.
When I call a method from my class ("InfoParse"), my conroller returns a blank page. I think this has to do with the fact that I'm calling a method from the library which uses Eloquent to interface with the database. I tried adding
use Illuminate\Database\Eloquent\Model;
to the top of the file, but that didn't help either.
Is there a specific way I should be setting up my class file so I can use either the DB:: class or Eloquent class?
Below is the function in question:
/**
* Check to see if this student is already recorded in our student table.
* If not, add the entry, then return true.
* #param int $cwid
* #return boolean
*/
public static function checkStudentTableRecords($cwid)
{
if(Student::where('cwid', '=', $cwid)->count() != 0)
{
return TRUE;
}
else
{ ##insert the student into our student table
$studentInfo = self::queryInfoFromCWID($cwid);
$studentEntry = new Student;
$studentEntry->cwid = $cwid;
$studentEntry->fName = $studentInfo['fName'];
$studentEntry->lName = $studentInfo['lName'];
$studentEntry->email = $studentInfo['email'];
$studentEntry->save();
return TRUE;
}
}
(note: the self::queryInfoFromCWID() function is calling a function defined earlier in the class)
After some investigation, it turns out I need to format my Eloquent Model calls like this:
if(\Student::where('cwid', '=', $cwid)->count() != 0)
...
$studentEntry = new \Student;
The backslash is necessary to avoid namespace collision within the Laravel4 application.

Categories