In my app, I use soft delete on a lot of object, but I still want to access them in my app, just showing a special message that this item has been deleted and give the opportunity to restore it.
Currently I have to do this for all my route parametters in my RouteServiceProvider:
/**
* Define your route model bindings, pattern filters, etc.
*
* #return void
*/
public function boot()
{
parent::boot();
Route::bind('user', function ($value) {
return User::withTrashed()->find($value);
});
Route::bind('post', function ($value) {
return Post::withTrashed()->find($value);
});
[...]
}
Is there a quicker and better way to add the trashed Object to the model binding ?
Jerodev's answer didn't work for me. The SoftDeletingScope continued to filter out the deleted items. So I just overrode that scope and the SoftDeletes trait:
SoftDeletingWithDeletesScope.php:
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletingScope;
class SoftDeletingWithDeletesScope extends SoftDeletingScope
{
public function apply(Builder $builder, Model $model)
{
}
}
SoftDeletesWithDeleted.php:
namespace App\Models\Traits;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Scopes\SoftDeletingWithDeletesScope;
trait SoftDeletesWithDeleted
{
use SoftDeletes;
public static function bootSoftDeletes()
{
static::addGlobalScope(new SoftDeletingWithDeletesScope);
}
}
This effectively just removes the filter while still allowing me to use all the rest of the SoftDeletingScope extensions.
Then in my model I replaced the SoftDeletes trait with my new SoftDeletesWithDeleted trait:
use App\Models\Traits\SoftDeletesWithDeleted;
class MyModel extends Model
{
use SoftDeletesWithDeleted;
For Laravel 5.6 to 7
You can follow this doc https://laravel.com/docs/5.6/scout#soft-deleting. And set the soft_delete option of the config/scout.php configuration file to true.
'soft_delete' => true,
For Laravel 8+
You can follow this doc https://laravel.com/docs/8.x/routing#implicit-soft-deleted-models. And append ->withTrashed() to the route that should accept trashed models:
Ex:
Route::get('/users/{user}', function (User $user) {
return $user->email;
})->withTrashed();
You can add a Global Scope to the models that have to be visible even when trashed.
For example:
class WithTrashedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->withTrashed();
}
}
class User extends Model
{
protected static function boot()
{
parent::boot();
static::addGlobalScope(new WithTrashedScope);
}
}
Update:
If you don't want to show the deleted objects you can still manually add ->whereNull('deleted_at') to your query.
Related
I have category policy as below partial code.
class CategoryPolicy
{
use HandlesAuthorization;
public function view(User $user, Category $category)
{
return true;
}
}
Then, I call from livewire component inside the mount method.
class Productcategorysetup extends Component
{
use CategoryPolicy;
public function mount()
{
$this->authorize('view',CategoryPolicy::class);
}
}
I got an error message
App\Http\Livewire\Generalsetting\Productcategorysetup cannot use App\Policies\CategoryPolicy - it is not a trait
Any advice or guidance on this would be greatly appreciated, Thanks.
To use authorization in Livewire, you need to import the AuthorizesRequests trait first, and use that in your class.
Secondly, the first argument to authorize() when using view, is the instance of a model - in your case, a category. But this sounds like you want to list categories, i.e. the "index" file - which means you want to check for viewAny (as view is for a specific resource). In that case, the second argument is the class-name of the model, rather than the instance of a model.
<?php
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use App\Models\Category;
class Productcategorysetup extends Component
{
use AuthorizesRequests;
public function mount()
{
$this->authorize('viewAny', Category::class);
}
}
Then in your policy,
class CategoryPolicy
{
use HandlesAuthorization;
public function viewAny(User $user)
{
return true;
}
public function view(User $user, Category $category)
{
return true;
}
}
I have following route group in my laravel 8.0 app:
Route::prefix('offline_transaction')->name('offline_transaction.')->group(function () {
Route::post('/approve/{transaction:uuid}', [OfflineTransactionController::class, 'approve'])
->name('approve');
Route::post('/reject/{transaction:uuid}', [OfflineTransactionController::class, 'reject'])
->name('reject');
});
And Transaction model is:
class Transaction extends Model implements CreditBlocker
{
//....
protected static function boot()
{
parent::boot();
static::addGlobalScope(new AuthUserScope());
}
//....
}
And this is my AuthUserScope:
class AuthUserScope implements Scope
{
private string $fieldName;
public function __construct($fieldName = 'user_id')
{
$this->fieldName = $fieldName;
}
public function apply(Builder $builder, Model $model)
{
$user = Auth::user();
if ($user) {
$builder->where($this->fieldName, $user->id);
}
}
}
Now the problem is when an admin wants to approve or reject a transaction, 404 Not found error will throws. How can I pass this?
Customizing The Resolution Logic
If you wish to define your own model binding resolution logic, you may
use the Route::bind method. The closure you pass to the bind
method will receive the value of the URI segment and should return the
instance of the class that should be injected into the route. Again,
this customization should take place in the boot method of your
application's RouteServiceProvider:
Solution
What you can do is change the parameter name(s) in your routes/web.php file for the specific route(s).
Route::prefix('offline_transaction')->name('offline_transaction.')->group(function () {
Route::post('/approve/{any_transaction}', [OfflineTransactionController::class, 'approve'])
->name('approve');
Route::post('/reject/{any_transaction}', [OfflineTransactionController::class, 'reject'])
->name('reject');
Note the any_transaction. Change that to whatever naming convention you find most convenient.
Then, in your app/Providers/RouteServiceProvider.php file, change your boot(...) method to something like this:
use App\Models\Transaction;
use Illuminate\Support\Facades\Route;
// ...
public function boot()
{
// ...
Route::bind('any_transaction', function($uuid) {
return Transaction::withoutGlobalScopes()->where('uuid', $uuid)->firstOrFail();
});
// ...
}
// ...
Then in your controller app/Http/Controllers/OfflineTransactionController.php file, access the injected model:
use App\Models\Transaction;
// ...
public function approve(Transaction $any_transaction) {
// ...
}
// ...
Credits: Using Route Model Binding without Global Scope #thomaskim
Addendum
If you would like to remove a specific global scope from the route model bound query, you may use
withoutGlobalScope(AuthUserScope::class) in the boot(...) method of the app/Providers/RouteServiceProvider.php file.
Another approach is that I can use Route::currentRouteNamed in AuthUserScope class as following, which I prefer to use instead of Route::bind:
<?php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
class AuthUserScope implements Scope
{
private string $fieldName;
public function __construct($fieldName = 'user_id')
{
$this->fieldName = $fieldName;
}
public function apply(Builder $builder, Model $model)
{
$user = Auth::user();
if ($user && !Route::currentRouteNamed('admin.*')) {
$builder->where($this->fieldName, $user->id);
}
}
}
I want to use strtolower() before saving data in database for 5 attributes,
I'm using this code in Model
public function setFirstNameAttribute($value)
{
$this->attributes['firstName'] = strtolower($value);
}
public function setLastNameAttribute($value)
{
$this->attributes['lastName'] = strtolower($value);
}
public function setUserNameAttribute($value)
{
$this->attributes['userName'] = strtolower($value);
}
... etc
Can I use the __construct method instead of the above code?
There are two ways first one, to use boot method directly (preferred for small changes in model like in your question)
Method 1 :
we can directly use the boot method,
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Mymodel extends Model
{
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// Remember that $model here is an instance of MyModel
$model->firstName = strtolower($model->firstName);
$model->lastName = strtolower($model->lastName);
$model->userName = strtolower($model->userName);
// ...... other attributes
});
}
}
Method 2 :
So we can use here a simple trait with a simple method for generating a strtolower() for a string.This is preferred when you have to do bigger changes in your model while performing operations in model like saving, creating etc. Or even if you want to use the same property in multiple models.
Create a trait MyStrtolower
<?php
namespace App\Traits;
trait MyStrtolower
{
public function mystrtolower($string)
{
return strtolower($string);
}
}
We can now attach this trait to any class that we want to have the mystrtolower method.
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use App\Traits\MyStrtolower;
class Mymodel extends Model
{
use MyStrtolower; // Attach the MyStrtolower trait to the model
public static function boot()
{
parent::boot();
static::saving(function ($model) {
// Remember that $model here is an instance of MyModel
$model->firstName = $model->mystrtolower($model->firstName);
$model->lastName = $model->mystrtolower($model->lastName);
$model->userName = $model->mystrtolower($model->userName);
// ...... other attributes
});
}
}
If you want to not repeat all these lines of code for every model you make, make the trait configurable using abstract methods so that you can dynamically pass the attribute names for which you want to lower case string, like employee_name is Employee Model and user_name in User Model.
I'm trying to catch the model event updating in laravel 7 but it doesn't fire.
This is the place where the model get's changed:
public function update(Request $request, Model $model) {
$model->update($request->input());
return new Resource($model);
}
I also tried this to update the values:
public function update(Request $request, Model $model) {
$model->attribute1 = $request->get('value1');
$model->attribute2 = $request->get('value2');
$model->attribute3 = $request->get('value3');
$model->save();
return new Resource($model);
}
And here I'm trying to catch the event within the bill model:
protected static function boot() {
static::updating(function ($model) {
// code
});
}
What am I doing wrong?
You have to call parent::boot() at the start of your boot method:
protected static function boot() {
parent::boot();
static::updating(function ($model) {
// code
});
}
Laravel 7 added a booted method to make it easier:
Adding booting / booted methods to Model
Currently, users who extend the boot method to add event listeners on
model events must remember to call parent::boot() at the start of
their method (or after). This is often forgotten and causes confusion
for the user. By adding these simple place-holder extension points we
can point users towards these methods instead which do not require
them to call any parent methods at all.
From the docs:
Instead of using custom event classes, you may register Closures that
execute when various model events are fired. Typically, you should
register these Closures in the booted method of your model:
<?php
namespace App;
use App\Scopes\AgeScope;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The "booted" method of the model.
*
* #return void
*/
protected static function booted()
{
static::created(function ($user) {
//
});
}
}
I need to pass a collection to every view; the collection contains the IDs of the items in the user's shopping cart. I've tried Service Providers and a BaseClass but neither worked as (apparently) Auth hasn't been registered at those points and only returns null.
What's the best way get records from an authenticated user and pass it to every view?
Edit: here's the relevant code
User.php
public static function getCart()
{
if (Auth::guest()) {
return [];
}
$collection = new \Illuminate\Database\Eloquent\Collection();
$collection = Auth::user()->cart()->pluck('post_id');
return $collection;
}
CartServiceProvider.php
namespace App\Providers;
use View;
use App\User;
use Illuminate\Support\ServiceProvider;
class CartServiceProvider extends ServiceProvider
{
public function boot()
{
View::share('cart', User::getCart());
}
public function register()
{
//
}
}
In any view...
<?php dd($cart); ?>
returns [] because Auth hasn't been registered yet, so the empty array is returned.
Found the answer on Laracasts and it seems to work quite well.
https://laracasts.com/discuss/channels/general-discussion/l5-service-provider-for-sharing-view-variables
From the OP #imJohnBon: "I managed to solve this issue by creating 2 files. First a ComposerServiceProvider which uses a wildcard to be applied to every view and not just particular views:"
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Contracts\View\Factory as ViewFactory;
class ComposerServiceProvider extends ServiceProvider {
public function boot(ViewFactory $view)
{
$view->composer('*', 'App\Http\ViewComposers\GlobalComposer');
}
public function register()
{
//
}
}
"And then the corresponding GlobalComposer where I share variables that should be available in all views:"
namespace App\Http\ViewComposers;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
class GlobalComposer {
public function compose(View $view)
{
$view->with('currentUser', Auth::user());
}
}