How do you override a class from the Laravel source code? - laravel

I need to make a change to the retrieveUser() function within Illuminate/Broadcasting/Broadcasters/Broadcaster.php.
The change works if I edit the class directly, but I have heard that you are not supposed to do that because it is difficult to track changes to the source code and because it will get overwritten when upgrading Laravel or when pushing to production.
So if I wanted to write my own modified retrieveUser() function for the Broadcaster class (it happens to be an abstract class which implements BroadcasterContract), then where and how would I do that?
Original function:
/**
* Retrieve the authenticated user using the configured guard (if any).
*
* #param \Illuminate\Http\Request $request
* #param string $channel
* #return mixed
*/
protected function retrieveUser($request, $channel)
{
$options = $this->retrieveChannelOptions($channel);
$guards = $options['guards'] ?? null;
if (is_null($guards)) {
return $request->user();
}
foreach (Arr::wrap($guards) as $guard) {
if ($user = $request->user($guard)) {
return $user;
}
}
}
New function:
protected function retrieveUser($request, $channel)
{
$options = $this->retrieveChannelOptions($channel);
$guards = $options['guards'] ?? null;
if (is_null($guards)) {
$token = $request->header('Token');
$id = Crypt::decrypt($token);
$user = User::find($id);
return $user;
}
foreach (Arr::wrap($guards) as $guard) {
if ($user = $request->user($guard)) {
return $user;
}
}
}
UPDATE
As #ggdx pointed out in the comments, I can override the class by doing class yourClass extends Illuminate\Broadcasting\Broadcasters\Broadcaster
However, I still don't know where to put this new class within the Laravel framework. I tried creating the new class in the /app route, but that did not work.

I'm not completely sure what you are trying to accomplish. But I think making a custom driver for a guard will do what you want. Looking at the docs https://laravel.com/docs/5.8/authentication#adding-custom-guards
You can do this in the boot method of your AuthServiceProvider.
Auth::viaRequest('custom-token', function ($request) {
return User::find(Crypt::decrypt($request->header('Token')));
});
Also, make sure to select it as the driver for your guard in your auth.php config file.

Related

Laravel 8 Fortify - 2FA only when the user logs in from a new device

I am implementing two-factor authentication (2FA) in my Laravel 8 application.
The 2FA is applied every time the user logs in. However, I don't really feel that 2FA is necessary every time, I even find it annoying. As a solution I am thinking of applying it only when the user connects from a new device. Is there someone who has already done it or who can give me a hint of the changes that would be necessary?
I have got it. Here are the steps I have followed:
In the config file fortify.php I have added
'pipelines' => [
'login' => [
App\Actions\Fortify\RedirectIfTwoFactorAuthenticatable::class,
Laravel\Fortify\Actions\AttemptToAuthenticate::class,
Laravel\Fortify\Actions\PrepareAuthenticatedSession::class,
]
]
I have added the field two_factor_cookies to the User class.
I have customized the RedirectIfTwoFactorAuthenticatable class of
Fortify:
<?php
namespace App\Actions\Fortify;
use Laravel\Fortify\Actions\RedirectIfTwoFactorAuthenticatable as DefaultRedirectIfTwoFactorAuthenticatable;
use Laravel\Fortify\TwoFactorAuthenticatable;
class RedirectIfTwoFactorAuthenticatable extends DefaultRedirectIfTwoFactorAuthenticatable
{
/**
* Handle the incoming request.
*
* #param \Illuminate\Http\Request $request
* #param callable $next
* #return mixed
*/
public function handle($request, $next)
{
$user = $this->validateCredentials($request);
if (optional($user)->two_factor_secret &&
in_array(TwoFactorAuthenticatable::class, class_uses_recursive($user)) &&
$this->checkIfUserDeviceHasNotCookie($user)) {
return $this->twoFactorChallengeResponse($request, $user);
}
return $next($request);
}
/**
* This checks if the user's device has the cookie stored
* in the database.
*
* #param \App\Models\User\User $user
* #return bool
*/
protected function checkIfUserDeviceHasNotCookie($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
return !in_array($two_factor_cookie,$two_factor_cookies);
}
}
In the FortifyServiceProvider I have added a customized TwoFactorLoginResponse.
<?php
namespace App\Providers;
use App\Actions\Fortify\CreateNewUser;
use App\Actions\Fortify\ResetUserPassword;
use App\Actions\Fortify\UpdateUserPassword;
use App\Actions\Fortify\UpdateUserProfileInformation;
use App\Http\Responses\FailedPasswordResetLinkRequestResponse;
use App\Http\Responses\FailedPasswordResetResponse;
use App\Http\Responses\LockoutResponse;
use App\Http\Responses\LoginResponse;
use App\Http\Responses\LogoutResponse;
use App\Http\Responses\PasswordResetResponse;
use App\Http\Responses\RegisterResponse;
use App\Http\Responses\SuccessfulPasswordResetLinkRequestResponse;
use App\Http\Responses\TwoFactorLoginResponse;
use App\Http\Responses\VerifyEmail;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\ServiceProvider;
use Laravel\Fortify\Contracts\FailedPasswordResetLinkRequestResponse as FailedPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\FailedPasswordResetResponse as FailedPasswordResetResponseContract;
use Laravel\Fortify\Contracts\LockoutResponse as LockoutResponseContract;
use Laravel\Fortify\Contracts\LoginResponse as LoginResponseContract;
use Laravel\Fortify\Contracts\LogoutResponse as LogoutResponseContract;
use Laravel\Fortify\Contracts\PasswordResetResponse as PasswordResetResponseContract;
use Laravel\Fortify\Contracts\RegisterResponse as RegisterResponseContract;
use Laravel\Fortify\Contracts\SuccessfulPasswordResetLinkRequestResponse as SuccessfulPasswordResetLinkRequestResponseContract;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
use Laravel\Fortify\Fortify;
class FortifyServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->registerResponseBindings();
}
/**
* Register the response bindings.
*
* #return void
*/
protected function registerResponseBindings()
{
$this->app->singleton(LoginResponseContract::class, LoginResponse::class);
$this->app->singleton(LogoutResponseContract::class, LogoutResponse::class);
$this->app->singleton(TwoFactorLoginResponseContract::class, TwoFactorLoginResponse::class);
$this->app->singleton(RegisterResponseContract::class, RegisterResponse::class);
$this->app->singleton(LockoutResponseContract::class, LockoutResponse::class);
$this->app->singleton(SuccessfulPasswordResetLinkRequestResponseContract::class, SuccessfulPasswordResetLinkRequestResponse::class);
$this->app->singleton(FailedPasswordResetLinkRequestResponseContract::class, FailedPasswordResetLinkRequestResponse::class);
$this->app->singleton(PasswordResetResponseContract::class, PasswordResetResponse::class);
$this->app->singleton(FailedPasswordResetResponseContract::class, FailedPasswordResetResponse::class);
}
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Fortify::ignoreRoutes();
Fortify::loginView(function () {
return view('auth.login');
});
Fortify::twoFactorChallengeView('auth.two-factor-challenge');
Fortify::confirmPasswordView(function (Request $request) {
if ($request->ajax()) {
return view('auth.confirm-password-form');
} else {
return view('auth.confirm-password');
}
});
Fortify::requestPasswordResetLinkView(function () {
return view('auth.forgot-password');
});
Fortify::resetPasswordView(function ($request) {
return view('auth.reset-password', ['request' => $request,'token' => $request->route('token')]);
});
Fortify::registerView(function () {
return view('auth.register');
});
Fortify::verifyEmailView(function () {
return view('auth.verify');
});
Fortify::createUsersUsing(CreateNewUser::class);
Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class);
Fortify::updateUserPasswordsUsing(UpdateUserPassword::class);
Fortify::resetUserPasswordsUsing(ResetUserPassword::class);
/*RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->email.$request->ip());
});*/
RateLimiter::for('two-factor', function (Request $request) {
return Limit::perMinute(5)->by($request->session()->get('login.id'));
});
}
}
Finally, the TwoFactorLoginResponse:
<?php
namespace App\Http\Responses;
use Illuminate\Http\JsonResponse;
use Laravel\Fortify\Contracts\TwoFactorLoginResponse as TwoFactorLoginResponseContract;
class TwoFactorLoginResponse implements TwoFactorLoginResponseContract
{
/**
* Create an HTTP response that represents the object.
*
* #param \Illuminate\Http\Request $request
* #return \Symfony\Component\HttpFoundation\Response
*/
public function toResponse($request)
{
$user = \Auth::user();
$this->storeCookieIfNotInDB($user);
$role = $user->role;
if ($request->wantsJson()) {
return new JsonResponse('', 204);
}
if ($role == "0") {
return redirect()->route('user.home');
} else {
return redirect()->route('admin.home');
}
}
/**
* Store the cookie if it is not in the database.
*
* #param \App\Models\User\User $user
* #return void
*/
protected function storeCookieIfNotInDB($user)
{
$two_factor_cookies = json_decode($user->two_factor_cookies);
if (!is_array($two_factor_cookies)){
$two_factor_cookies = [];
}
$two_factor_cookie = \Cookie::get('2fa');
if (!in_array($two_factor_cookie,$two_factor_cookies)) {
$two_factor_cookie = md5(now());
$two_factor_cookies[] = $two_factor_cookie;
if (count($two_factor_cookies) > 3) {
array_shift($two_factor_cookies);
}
$user->two_factor_cookies = json_encode($two_factor_cookies);
$user->save();
$lifetime = 60 * 24 * 365; //one year
\Cookie::queue('2fa',$two_factor_cookie,$lifetime);
}
}
}
Upon login, it will look for the cookie 2fa. If its content is stored in the database, it will not be necessary to enter the code again. To prevent unlimited cookie content from being saved in the DB you can add a maximum limit (I have set it 3).
Thanks to Maarten Veerman for the inital help.
According to this line: https://github.com/laravel/fortify/blob/82c99b6999f7e89f402cfd7eb4074e619382b3b7/src/Http/Controllers/AuthenticatedSessionController.php#L80
you can create a pipelines.login entry in your fortify config file.
The solution would be to:
create the config entry
copy the pipeline setup in the above file, line 84.
create a custom AttemptToAuthenticate class, make sure the pipeline config points to your new class.
make the new class extend the default fortify AttemptToAuthenticate class.
overwrite the handle function, add your logic in the new function, where you check for a cookie on the device.

Laravel Relationship Find UUID

I have make a Trait for UUID. I use a lot of relationschip inside my code. On a relationship you can do find() and findOrFail() but i have write a code for findU() and findUOrFail() but i can't use it inside a relationship. How can i fix it?
Trait:
<?php
namespace App\Modules\Base\Traits;
use Ramsey\Uuid\Uuid;
/**
* Trait Uuids
*
* #package Modules\Core\Traits
*/
trait Uuids
{
/**
* Boot function from laravel.
*/
public static function bootUuids ()
{
static::creating(function ($model) {
$model->uuid = Uuid::uuid4()->toString();
});
}
/**
* #param $uuid
*
* #return mixed
*/
public static function findU ($uuid)
{
return static::where('uuid', '=', $uuid)->first();
}
/**
* #param $uuid
*
* #return mixed
*/
public static function findUOrFail($uuid)
{
$post = static::where('uuid', '=', $uuid)->first();
if( is_null($post) ) {
return abort(404);
} else {
return $post;
}
}
}
Controller:
/**
* Show
*/
public function show(Request $request, $uuid)
{
return responder()->success($request->user()->projects()->findUOrFail($uuid))->respond();
}
Error:
Call to undefined method Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany::findUOrFail()
Assuming you don't need id since you're using uuid
In your migration file you need:
$table->uuid('uuid');
$table->primary('uuid');
In your model:
use Uuids;
protected $primaryKey = 'uuid';
public $incrementing = false;
Or much easier
In your migration file:
$table->uuid('id');
$table->primary('id');
In your model:
use Uuids;
public $incrementing = false;
You don't need to override findOrFail or find
It should help to have the function referenced directly in the model rather than trying to access it directly in a trait. I am assuming that you are including the Uuids trait above in your projects model. If so, try creating a method on the projects model like this:
public function tryFindUOrFail($uuid)
{
return $this->findUOrFail($uuid);
}
Then you would write your show method as:
return responder()->success($request->user()->projects()->tryFindUOrFail($uuid))->respond();
If this doesn't work, you may need to include your method with the $appends array so that it is directly accessible through the relationship.

Laravel 5.3 Passport Custom Grants?

I know I am not the only person who has come up to this point. Does anyone know how to properly implement a custom grant in Laravel(5.3) Passport?
Or
Have a good link/tutorial to reference how to properly do it?
I know there's this package:
https://github.com/mikemclin/passport-custom-request-grant
But I'm asking for a more "Do it yourself" approach.
Thank you in advance.
namespace App\Providers;
use App\Auth\Grants\FacebookGrant;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Laravel\Passport\Bridge\RefreshTokenRepository;
use Laravel\Passport\Passport;
use League\OAuth2\Server\AuthorizationServer;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
app(AuthorizationServer::class)->enableGrantType(
$this->makeFacebookGrant(), Passport::tokensExpireIn()
);
Passport::routes();
//
}
/**
* Create and configure a Facebook grant instance.
*
* #return FacebookGrant
*/
protected function makeFacebookGrant()
{
$grant = new FacebookGrant(
$this->app->make(RefreshTokenRepository::class)
);
$grant->setRefreshTokenTTL(Passport::refreshTokensExpireIn());
return $grant;
}
}
EDIT:
sorry for only posting this code, i do not know how much this code is going to be useful to you.
Well, here i'll leave my implementation of FacebookGrant, hope this helps someone.
<?php
namespace App\Auth\Grants;
use Illuminate\Http\Request;
use Laravel\Passport\Bridge\User;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\AbstractGrant;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\RequestEvent;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ServerRequestInterface;
use RuntimeException;
class FacebookGrant extends AbstractGrant
{
/**
* #param RefreshTokenRepositoryInterface $refreshTokenRepository
*/
public function __construct(
RefreshTokenRepositoryInterface $refreshTokenRepository
) {
$this->setRefreshTokenRepository($refreshTokenRepository);
$this->refreshTokenTTL = new \DateInterval('P1M');
}
/**
* {#inheritdoc}
*/
public function respondToAccessTokenRequest(
ServerRequestInterface $request,
ResponseTypeInterface $responseType,
\DateInterval $accessTokenTTL
) {
// Validate request
$client = $this->validateClient($request);
$scopes = $this->validateScopes($this->getRequestParameter('scope', $request));
$user = $this->validateUser($request, $client);
// Finalize the requested scopes
$scopes = $this->scopeRepository->finalizeScopes($scopes, $this->getIdentifier(), $client, $user->getIdentifier());
// Issue and persist new tokens
$accessToken = $this->issueAccessToken($accessTokenTTL, $client, $user->getIdentifier(), $scopes);
$refreshToken = $this->issueRefreshToken($accessToken);
// Inject tokens into response
$responseType->setAccessToken($accessToken);
$responseType->setRefreshToken($refreshToken);
return $responseType;
}
/**
* #param ServerRequestInterface $request
*
* #return UserEntityInterface
* #throws OAuthServerException
*/
protected function validateUser(ServerRequestInterface $request, ClientEntityInterface $client)
{
$facebookId = $this->getRequestParameter('facebook_id', $request);
if (is_null($facebookId)) {
throw OAuthServerException::invalidRequest('facebook_id');
}
$email = $this->getRequestParameter('email', $request);
if (is_null($email)) {
throw OAuthServerException::invalidRequest('email');
}
$user = $this->getUserEntityByUserFacebookId(
$facebookId,
$email,
$this->getIdentifier(),
$client
);
if ($user instanceof UserEntityInterface === false) {
$this->getEmitter()->emit(new RequestEvent(RequestEvent::USER_AUTHENTICATION_FAILED, $request));
throw OAuthServerException::invalidCredentials();
}
return $user;
}
/**
* Retrieve a user by the given Facebook Id.
*
* #param string $facebookId
* #param string $email
* #param string $grantType
* #param \League\OAuth2\Server\Entities\ClientEntityInterface $clientEntity
*
* #return \Laravel\Passport\Bridge\User|null
* #throws \League\OAuth2\Server\Exception\OAuthServerException
*/
private function getUserEntityByUserFacebookId($facebookId, $email, $grantType, ClientEntityInterface $clientEntity)
{
$provider = config('auth.guards.api.provider');
if (is_null($model = config('auth.providers.'.$provider.'.model'))) {
throw new RuntimeException('Unable to determine authentication model from configuration.');
}
$user = (new $model)->where('facebook_id', $facebookId)->first();
if (is_null($user)) {
$user = (new $model)->where('email', $email)->first();
if (is_null($user)) {
return;
}
// Now that we retrieved the user with the email, we need to update it with
// the given facebook id. So the user account will be linked correctly.
$user->facebook_id = $facebookId;
$user->save();
}
return new User($user->getAuthIdentifier());
}
/**
* {#inheritdoc}
*/
public function getIdentifier()
{
return 'facebook';
}
}
I am not sure what do you mean by custom grant but you can use passport service for password grant which can be customized however you like.
Reference: https://laravel.com/docs/5.3/passport#password-grant-tokens
To give more insight, you get a client_id and client_secret which all users of your api will use to get access_token and refresh_token using their password and email as well and then you can customize the process by adding features via middleware.
For example you can check some customized header if present or change database based on request to perform further query and so on.
Maybe I got whole wrong idea about your ques, if that's the case then please specify more about your needs.

Laravel authentication without global scope

In my Laravel app users can disable (not delete) their account to disappear from the website. However, if they try to login again their account should be activated automatically and they should log in successfully.
This is done with "active" column in the users table and a global scope in User model:
protected static function boot() {
parent::boot();
static::addGlobalScope('active', function(Builder $builder) {
$builder->where('active', 1);
});
}
The problem now is that those inactive accounts can't log in again, since AuthController does not find them (out of scope).
What I need to achieve:
Make AuthController ignore global scope "active".
If username and password are correct then change the "active" column value to "1".
The idea I have now is to locate the user using withoutGlobalScope, validate the password manually, change column "active" to 1, and then proceed the regular login.
In my AuthController in postLogin method:
$user = User::withoutGlobalScope('active')
->where('username', $request->username)
->first();
if($user != null) {
if (Hash::check($request->username, $user->password))
{
// Set active column to 1
}
}
return $this->login($request);
So the question is how to make AuthController ignore global scope without altering Laravel main code, so it will remain with update?
Thanks.
Create a class GlobalUserProvider that extends EloquentUserProvider like below
class GlobalUserProvider extends EloquentUserProvider {
public function createModel() {
$model = parent::createModel();
return $model->withoutGlobalScope('active');
}
}
Register your new user provider in AuthServiceProvider:
Auth::provider('globalUserProvider', function ($app, array $config) {
return new GlobalUserProvider($this->app->make('hash'), $config['model']);
});
Finally you should change your user provider driver to globalUserProvider in auth.php config file.
'providers' => [
'users' => [
'driver' => 'globalUserProvider',
'model' => App\Models\User::class
]
]
protected static function boot()
{
parent::boot();
if (\Auth::check()) {
static::addGlobalScope('active', function(Builder $builder) {
$builder->where('active', 1);
});
}
}
Please try this for login issue, You can activate after login using withoutGlobalScopes().
#Sasan's answer is working great in Laravel 5.3, but not working in 5.4 - createModel() is expecting a Model but gets a Builder object, so when EloquentUserProvider calls $model->getAuthIdentifierName() an exception is thrown:
BadMethodCallException: Call to undefined method Illuminate\Database\Query\Builder::getAuthIdentifierName() in /var/www/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:2445
Instead, follow the same approach but override more functions so that the right object is returned from createModel().
getQuery() returns the builder without the global scope, which is used by the other two functions.
class GlobalUserProvider extends EloquentUserProvider
{
/**
* Get query builder for the model
*
* #return \Illuminate\Database\Eloquent\Builder
*/
private function getQuery()
{
$model = $this->createModel();
return $model->withoutGlobalScope('active');
}
/**
* Retrieve a user by their unique identifier.
*
* #param mixed $identifier
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveById($identifier)
{
$model = $this->createModel();
return $this->getQuery()
->where($model->getAuthIdentifierName(), $identifier)
->first();
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* #param mixed $identifier
* #param string $token
* #return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
{
$model = $this->createModel();
return $this->getQuery()
->where($model->getAuthIdentifierName(), $identifier)
->where($model->getRememberTokenName(), $token)
->first();
}
}
Sasan Farrokh has a right answer. The only thing not to rewrite createModel but newModelQuery and this will work
protected function newModelQuery($model = null)
{
$modelQuery = parent::newModelQuery();
return $modelQuery->withoutGlobalScope('active');
}
Extend the AuthController with the code you used in your OP. That should work.
public function postLogin(Request $request)
{
$user = User::withoutGlobalScope('active')
->where('username', $request->username)
->first();
if($user != null){
if (Hash::check($request->password, $user->password)){
$user->active = 1;
$user->save();
}
}
return $this->login($request);
}
I resolved it by creating the new package.
mpyw/scoped-auth: Apply specific scope for user authentication.
Run composer require mpyw/scoped-auth and modify your User model like this:
<?php
namespace App;
use Illuminate\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Mpyw\ScopedAuth\AuthScopable;
class User extends Model implements UserContract, AuthScopable
{
use Authenticatable;
public function scopeForAuthentication(Builder $query): Builder
{
return $query->withoutGlobalScope('active');
}
}
You can also easily pick Illuminate\Auth\Events\Login to activate User on your Listener.
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event listener mappings for the application.
*
* #var array
*/
protected $listen = [
\Illuminate\Auth\Events\Login::class => [
\App\Listeners\ActivateUser::class,
],
];
/**
* Register any events for your application.
*
* #return void
*/
public function boot()
{
parent::boot();
//
}
}
 
<?php
namespace App\Listeners;
use Illuminate\Auth\Events\Login;
class ActivateUser
{
/**
* Handle the event.
*
* #param Illuminate\Auth\Events\Login $event
* #return void
*/
public function handle(Login $event)
{
$event->user->fill('active', 1)->save();
}
}
 
I had to use
->withoutGlobalScopes() instead
in order for it to work

Unable to override automatic model find method calls since upgrading to Laravel 5.1

I have a simple trait which I use to always include soft-deleted items for a few things:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return parent::withTrashed()->find($id, $columns);
}
}
However, since upgrading to Laravel 5.1, this no longer works. Soft-deleted items do not turn up in get() lists, and if I try to access a page where I've used route model bindings, I get the NotFoundHttpException.
Laravel's upgrade documentation states that:
If you are overriding the find method in your own models and calling parent::find() within your custom method, you should now change it to call the find method on the Eloquent query builder:
So I changed the trait accordingly:
trait OverrideTrashedTrait {
public static function find($id, $columns = ['*'])
{
return static::query()->withTrashed()->find($id, $columns);
}
}
But it appears that no matter what I write in there, it doesn't affect the results. I have also tried to put the overriding find() method directly in the model, but that doesn't appear to be working either. The only way anything changes is if I write invalid syntax. Even if I change the $id to a hardcoded id of an item that is not soft-deleted, I get the same result, and absolutely nothing happens if I e.g. try to dd('sdfg'), so I doubt the method is even called.
Edit: If I do trigger the method manually, it works just like intended.
How can I fix this?
Ok here it goes:
short version: Model binding does not use find.
longer version:
/**
* Register a model binder for a wildcard.
*
* #param string $key
* #param string $class
* #param \Closure|null $callback
* #return void
*
* #throws NotFoundHttpException
*/
public function model($key, $class, Closure $callback = null)
{
$this->bind($key, function ($value) use ($class, $callback) {
if (is_null($value)) {
return;
}
// For model binders, we will attempt to retrieve the models using the first
// method on the model instance. If we cannot retrieve the models we'll
// throw a not found exception otherwise we will return the instance.
$instance = new $class;
if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) {
return $model;
}
// If a callback was supplied to the method we will call that to determine
// what we should do when the model is not found. This just gives these
// developer a little greater flexibility to decide what will happen.
if ($callback instanceof Closure) {
return call_user_func($callback, $value);
}
throw new NotFoundHttpException;
});
}
Line 931 of Illuminate\Routing\Router says it does:
$instance->where($instance->getRouteKeyName(), $value)->first()
It uses the key from the model used in a where and loads the first result.
In Laravel 5.1 find() method can be found in Illuminate\Database\Eloquent\Builder
From your Model class you can override it like following:
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
/**
* Overrides find() method in Illuminate\Database\Eloquent\Builder.
* Finds only active products.
*
* #param mixed $id
* #param array $columns
* #return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|null
*/
public static function find($id, $columns = ['*']) {
// create builder from the Model
$builder = (new self)->newQuery();
// method customization
if (is_array($id)) {
// findMany() also should be customized
return self::findMany($id, $columns);
}
$builder->getQuery()->where("isActive", '=', 1)->where($builder->getModel()->getQualifiedKeyName(), '=', $id);
return $builder->first($columns);
}
}

Resources