Laravel custom collection or custom query builder? - php

I have that project that have orders that have status, and every status can be seen depending on if the user have the permission to manage it. I've tried to use custom collection but the downside is it's not paginatiable by default,
here's the code
class OrdersCollection extends Collection
{
public function allowedForUser(User $user)
{
return $this->filter(function ($order) use ($user) {
return $user->can(sprintf('manage %s orders', strtolower($order->status)));
});
}
}
here's how I'm trying to use it
$orders = Order::withoutTrashed()
->allowedForUser(Auth::user())
->paginate(50);
EDIT:
to avoid filtering through 20k+ rows in the table I came up with this custom builder
class OrderBuilder extends Builder
{
public function WhereAllowedForUser(User $user)
{
$permissions = $user->permissions()
->where('name', 'like', 'manage % orders')
->pluck('name')
->map(function ($item) {
return str_replace('manage ', '', str_replace(' orders', '', $item));
});
return $this->whereIn('status', $permissions);
}
}

You have to use scoped queries for your Model.
The idea is for you to pass a value to the scope and do what you want/need with it:
class Order extends Model
{
public function scopeWhereAllowedForUser($query, User $user)
{
$permissions = $user->permissions()
->where('name', 'like', 'manage % orders')
->pluck('name')
->map(function ($item) {
return str_replace('manage ', '', str_replace(' orders', '', $item));
});
return $query->whereIn('status', $permissions);
}
}
So you use it like this:
$orders = Order::whereAllowedForUser(Auth::user())->paginate(50);
I am not 100% sure what you are trying to do with that $permissions query, I am not sure what you tried to do, so explain it a little more and I can help you with it (better code).

Related

Filter a collection in Laravel

i want to filter a collection in order to return only items with fullname that is like the one given in parmaters
public function searchFriend($fullName){
return Auth::user()->friends->filter(function ($item) use ($fullName) {
return false !== stristr($item->friend->full_name, $fullName);
});
}
the function actually is not returning the correct results.
Instead of accessing the collection directly, you can do the filtering in SQL.
public function searchFriend($fullName){
return Auth::user()
->friends()
->where('full_name', 'like', '%'.$fullName.'%')
->get();
}
If you, for whichever reason, need to do it on the collection, then the problem with your current code is that $item represents the friend, so you're checking $item->friend->full_name instead of $item->full_name.
public function searchFriend($fullName){
return Auth::user()
->friends
->filter(function ($item) use ($fullName) {
return false !== stristr($item->full_name, $fullName);
});
}
I guess friend is a relationship to the User model so it could be
public function searchFriend($fullName)
{
return Auth::user()->friends()->whereHas('friend', function ($query) use ($fullName) {
$query->where('full_name', 'like', "%{$fullName}%");
})->get();
}
If you have first_name and last_name in User model then you can use concat.
public function searchFriend($fullName)
{
return Auth::user()->friends()->whereHas('friend', function ($query) use ($fullName) {
$query->whereRaw("concat(first_name, ' ', last_name) like ?", ["%{$fullName}%"]);
})->get();
}

Detach role from the user and delete the user in bulk along with livewire traits

Description
I have a "belongsToMany" eloquent relationship between users and roles . I am creating a CRUD system with capability to do bulk actions. I followed the Livewire Screencasts Bulk Export/Delete and Refactoring For Re-Usability. I have created a trait for the bulk actions, so I'm able to use it out of the box.
I need to detach the role from the user and then delete the user in bulk. I am unable to call roles relationship on a public method property and detach the same. This is how I detach the role for a single user $this->user->roles()->detach(); but I'm unable to do it with $this->selectedRowsQuery->roles()->detach(); in case of bulk user deletion.
Stripped-down, copy-pastable code snippets
Livewire/Backend/UserController
public $showUserBulkDeletionModal = false;
public function confirmDeleteBulk()
{
$deleteCount = $this->selectedRowsQuery->count();
$this->selectedRowsQuery->roles()->detach();
$this->selectedRowsQuery->delete();
$this->showUserBulkDeletionModal = false;
$this->notify('You\'ve deleted '.$deleteCount.' users');
}
public function getRowsQueryProperty()
{
$query = User::query()
->when($this->filters['email'], fn($query, $email) => $query->where('email', 'like', '%'.$email.'%'))
->when($this->filters['role'], fn($query, $role) => $query->whereHas('roles', fn ($query) => $query->where('id', $role)))
->when($this->filters['search'], fn($query, $search) => $query->where('name', 'like', '%'.$search.'%'))
->when($this->filters['date-min'], fn($query, $created_at) => $query->where('created_at', '>=', Carbon::createFromFormat('d/m/Y', $created_at)))
->when($this->filters['date-max'], fn($query, $created_at) => $query->where('created_at', '<=', Carbon::createFromFormat('d/m/Y', $created_at)));
return $this->applySorting($query);
}
Livewire/Traits/WithBulkActions
trait WithBulkActions
{
public $selectPage = false;
public $selectAll = false;
public $selected = [];
public function renderingWithBulkActions()
{
if ($this->selectAll) $this->selectPageRows();
}
public function updatedSelected()
{
$this->selectAll = false;
$this->selectPage = false;
}
public function updatedSelectPage($value)
{
if ($value) return $this->selectPageRows();
$this->selectAll = false;
$this->selected = [];
}
public function selectPageRows()
{
$this->selected = $this->rows->pluck('id')->map(fn($id) => (string) $id);
}
public function selectAll()
{
$this->selectAll = true;
}
public function getSelectedRowsQueryProperty()
{
return (clone $this->rowsQuery)
->unless($this->selectAll, fn($query) => $query->whereKey($this->selected));
}
}
Context
Livewire version: 2.3.6
Laravel version: 8.23.1
Alpine version: 2.8.0
Browser: Chrome
This line won't work:
$this->selectedRowsQuery->roles()->detach();
$this->selectedRowsQuery is a Collection, and your code isn't smart enough to know which instance of roles() you're trying to detach (delete). You simply need to do this in a loop:
foreach ($this->selectedRowsQuery as $queryRow) {
$queryRow->roles()->detach();
$queryRow->delete(); // Can do this here (single delete), or below
}
$this->selectedRowsQuery->delete(); // Can do this here (batch delete), or above
Edit: At the time of the foreach(), $this->selectedRowsQuery is still an instance of the Builder class, which is incompatible with foreach() until a Closure (get(), cursor(), etc.) is passed. To handle this, simply adjust your code as:
foreach ($this->selectedRowsQuery->get() as $queryRow) {
$queryRow->roles()->detach();
...
}
Note: ->get() is more widely used, but ->cursor() is available and generally more performant for larger loops.

Search Data from multiple tables laravel

I am trying to search multiple data from two related tables. To be specific I want to get only "name column" from the users table and the rest of the columns from the posts table. But whenever I tried to search it prints the following error "Trying to get property 'name' of non-object"
Below is my user model
<?php
namespace App;
use App\Mail\NewUserWelcomeMail;
use Illuminate\Notifications\Notifiable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Mail;
class User extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name', 'email','phone', 'username', 'password',
'admin', 'address', 'description', 'approved_at',
];
protected $hidden = [
'password', 'remember_token',
];
public function posts()
{
return $this->hasMany(Post::class)->orderBy('created_at', 'DESC');
}
}
And post model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $guarded = [];
public function user()
{
return $this->belongsTo(User::class);
}
}
And my Controller
public function showcampaign(User $user) {
$q = Input::get( 'q' );
if( !empty( $q ) ) {
$showcampaign = User::join('posts','posts.user_id','users.id')
->where('name','LIKE','%'.$q.'%')
->orWhere('caption','LIKE','%'.$q.'%')
->orWhere('description','LIKE','%'.$q.'%')
->orWhere('duration','LIKE','%'.$q.'%')
->orWhere('amount','LIKE','%'.$q.'%')
->get();
if(count($showcampaign) > 0) {
return view('admin.campaignreport', ['show' => $showcampaign]);
} else {
return redirect('/campaignreport')->with('status', 'No Details found. Try to search again !');
}
} else {
$showcampaign = Post::all();
return view('admin.campaignreport')->with('show', $showcampaign);
}
}
Please help thanks
As you have already declared the relations within the Model So you can use whereHas and also orWhereHas
So
$showcampaign = SampleReception::query()
->whereHas('posts',function(\Illuminate\Database\Eloquent\Builder $query) use ($q){
return $query->where('caption', 'LIKE','%'.$q.'%')
->orWhere('description', 'LIKE','%'.$q.'%')
->orWhere('duration', 'LIKE','%'.$q.'%')
->orWhere('amount', 'LIKE','%'.$q.'%');
})
->orWhere('name','LIKE','%'.$q.'%')
->get();
For any issues leave a comment
Try.. use where instead of orwhere
$showcampaign = User::join('posts','posts.user_id','users.id')
->where('name','LIKE','%'.$q.'%')
->Where('caption','LIKE','%'.$q.'%')
->Where('description','LIKE','%'.$q.'%')
->Where('duration','LIKE','%'.$q.'%')
->Where('amount','LIKE','%'.$q.'%')->get();
I think you need to use a reference table for where clause.
$showcampaign = User::join('posts','posts.user_id', '=', 'users.id')
->where('users.name','LIKE', '%'.$q.'%')
->orWhere('posts.caption', 'LIKE','%'.$q.'%')
->orWhere('posts.description', 'LIKE','%'.$q.'%')
->orWhere('posts.duration', 'LIKE','%'.$q.'%')
->orWhere('posts.amount', 'LIKE','%'.$q.'%')
->get();
If you define relationship correctly then use:
$showcampaign = SampleReception::with(['posts' => function($query) use($q) {
return $query->where('caption', 'LIKE','%'.$q.'%')
->orWhere('description', 'LIKE','%'.$q.'%')
->orWhere('duration', 'LIKE','%'.$q.'%')
->orWhere('amount', 'LIKE','%'.$q.'%');
}])
->orWhere('name','LIKE','%'.$q.'%')
->get();
I use scope query to create search functions. It makes my code more flexible and easy to understand.
Here is how you can do this.
In your "user" Model, you have to write "scopeFunctionName". you can write any functionName with scope. And in your Controller, you can simply call this function whenever you need as functionName(). and pass here your search term from request as functionName(request(['search']))
And now as you have declared relations for these tables to search data from related tables, try using this code in your user Model
public function scopeFilter($query, array $filters)
{
/*
Hey $query! whenever u get 'search term' in URL, call the function.
If ['search'] exists or != null, then pass its value to the function
else return false and don't execute the $query. If value exists,
Then the function will execute the query and returns a result.
*/
$query->when(
$filters['search'] ?? false,
fn ($query, $search) =>
$query
->whereHas('users', function ($query) use ($search) {
return $query
->where('name', 'like', '%' . $search . '%')
})
->where('name', 'like', '%' . $search . '%')
->orWhere('caption', 'like', '%', . $search . '%')
->orWhere('description', 'like', '%', . $search . '%')
->orWhere('id', $search)
);
}
And in Controller, you can pass the value to the scope function as:
public function index()
{
return view('posts.index', [
'show' => Post::all()->filter(request(['search']))->with('user')
]);
}
Note: The code above has not been tested but it might solve your problem.

Filtering Laravel resource collections is there a better way

I am experimenting with creating a Restful API in Laravel and I am making use of the resources feature designed for API output. I have created the following models:
Book, Author and Category. I also created a resource for each of these models.
There is a one to many relationship between author and book and a many to many between category and book which has a pivot table.
I can return a collection of all books easily with the following:
return BookResource::collection(Book::with(['author', 'categories'])->paginate(10));
But I want to easily filter by author and category so I have implemented it in the following way inside my controller:
public function index(request $request)
{
//if no filer params are passed in return all books with author and categories
if (!$request->has('author') && !$request->has('category')) {
return BookResource::collection(Book::with(['author', 'categories'])->paginate(10));
}
//author param is passed in
if($request->has('author') && !$request->has('category')){
$authorName = $request->author;
return BookResource::collection(Book::whereHas('author', function ($query) use ($authorName) {
$query->where('name', $authorName);
})->get());
}
//category param is passed in
if(!$request->has('author') && $request->has('category')){
$categoryName = $request->category;
return BookResource::collection(Book::whereHas('categories', function ($query) use ($categoryName) {
$query->where('name', $categoryName);
})->get());
}
}
Is there a better more efficient way of returning the BookResource collection filtered by author and category?
Please try to implement this way. Hope this helps. Thanks.
public function index(){
$author = request ('author', null);
$category = request ('category', null);
$books = Book::with(['author', 'categories'])->when($author, function ($query) use ($author) {
return $query->whereHas('author', function ($query) use ($author){
$query->where('name', $author);
});
})->when($category, function ($query) use ($category) {
return $query->whereHas('categories', function ($query) use ($category) {
$query->where('name', $category);
});
})->paginate(10);
return BookResource::collection($books);
}

Laravel repository pattern add query

I am making a repository pattern in Laravel, and I have made an AbstractRepository class which is extended by any repository to get the most used CRUD methods which can be shared.
Now I can extend the main functionality by adding additional methods to concrete repositories if I need some more complex queries.
For example:
public function eagerWhere($column, $value, $relation, $orderBy = 'name')
{
return Region::with($relation)->where($column, $value)->orderBy($orderBy);
}
Now the part I'm having trouble with is this part in the main code which uses my repository:
$regions = $this->regionRepository->eagerWhere('name', $term, 'country');
if ($includeCountry) { //<-- from here
$regions->orWhereHas('country', function ($query) use ($term) {
$query->where('name', 'LIKE', '%' . $term . '%');
});
}
How can I write that part in the repository so that ultimately I can make it look like:
$regions = $this->regionRepository->eagerWhere('name', $term, 'country');
if ($includeCountry) {
$regions->orWhereHas($term, 'country');
}
I tried copying that part of the code to repository, but then I can't chain methods because when $region is fetched, it is no longer considered to be a repository instance, but Eloquent one. And now it is expecting Eloquent method instead.
I think your level of abstractation its a bit mixed, since you are not abstracting the model itself also, but anyways. A solution might be like this :
public function eagerWhere($column, $value, $relation)
{
$builder = Region::with($relation)->where($column, $value);
return $builder
}
then:
$regions = $this->regionRepository->eagerWhere('name', $term, 'country');
if ($includeCountry) {
$regions->orWhereHas($term, 'country');
}
return $regions->orderBy('name')->get();
I didn't manage to do quite what I wanted, but the fix that was satisfying solution was to put the complete logic inside the method, so now I have this:
public function eagerWhereLike($column, $value, $relation, $searchOnRelation = false, $orderBy = 'name')
{
$regions = Region::with($relation)->where($column, 'LIKE', '%' . $value . '%')->orderBy($orderBy);
if ($searchOnRelation) {
$regions->orWhereHas($relation, function ($query) use ($column, $value) {
$query->where($column, 'LIKE', '%' . $value . '%');
});
}
return $regions;
}

Categories