I'm using Laravel 5.0. I need to be able to change the value of the session lifetime in config/session.php from the front end, making the value configurable to an admin user of my site.
In the docs I've read that you can get/set variables using the config helper function, like so:
config(['session.lifetime' => '60']);
config('session.lifetime'); // '60'
But it it only changes the configuration value for that request. How do I persist this configuration, making it work across all requests?
This answer may be dumb, try to session flash an object that has the changes. Then implant a middleware that takes what is flashed in session and redo the config changes, then reflash.
$whatever_to_change = ['session.lifetime' => '60'];
session()->flash('changes', $whatever_to_change);
config($whatever_to_change);
In a middleware:
$from_flash = session()->get('changes');
config($whatever_to_change);
session()->keep(['changes']);
Then use this middleware in your other routes. I think this may not work if you want to change session driver.
I would recommend implementing a custom session driver that reads the lifetime value out of the database.
It is pretty easy and you'll only have to override the GC (garbage collection) method, as well as creating a database table to store this value.
I've done it in the past to make soft-deleting database sessions for analytics purposes. Let me know if you need any code snippets to proceed.
Edit: with code, my problem was slightly different, so I have edited in some key components but this doesn't pretend to be a complete solution:
I created a folder called Library\Session under app to store this new SessionHandler in:
<?php
namespace App\Library\Session;
use DB;
class DynamicallyConfiguredDatabaseSessionHandler extends \Illuminate\Session\DatabaseSessionHandler
{
/**
* {#inheritdoc}
*/
public function read($sessionId)
{
$session = (object) $this->getQuery()->find($sessionId);
if (isset($session->payload)) {
$this->exists = true;
return base64_decode($session->payload);
}
}
/**
* {#inheritdoc}
*/
public function write($sessionId, $data)
{
if ($this->exists) {
$this->getQuery()->where('id', $sessionId)->update([
'payload' => base64_encode($data), 'last_activity' => time(),
]);
} else {
$this->getQuery()->insert([
'id' => $sessionId, 'payload' => base64_encode($data), 'last_activity' => time()
]);
}
$this->exists = true;
}
/**
* {#inheritdoc}
*/
public function destroy($sessionId)
{
$this->getQuery()->where('id', $sessionId)->delete();
}
/**
* {#inheritdoc}
*/
public function gc($lifetime)
{
$dynamic_lifetime = DB::select('select lifetime from config limit 1');
$this->getQuery()->where('last_activity', '<=', time() - $dynamic_lifetime)->delete();
}
}
Then in config/session.php set 'driver' => 'dynamically_configured_database'
Then in App\Providers\AppServiceProvider use this boot() method:
public function boot()
{
//This is seriously the only way to modify how sessions work
\Session::extend('dynamically_configured_database', function() {
$connection = $this->app['config']['session.connection'];
$connection = $this->app['db']->connection($connection);
$table = $this->app['config']['session.table'];
return new \App\Library\Session\DynamicallyConfiguredDatabaseSessionHandler($connection, $table);
});
}
To ensure that the session is destroyed at the end of the specified period every time, change the garbage collection lottery odds in session.php like so:
'lottery' => [1, 1],
Prepare for this to take a little time, and I would love to hear your lessons learned about easier ways to add in custom database drivers, but once I got it working it hasn't had any problems yet.
Related
I have an Observer set up to Listen to a Model's events in order to keep my Controller clean of Logging messages. My implementation is as follows:
First, a store method that does just what it's supposed to do. Create and save a new model from valid parameters.
# app/Http/Controllers/ExampleController.php
namespace App\Http\Controllers;
use App\Http\Requests\StoreExample;
use App\Example;
class ExampleController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Create and save an Example from validated form parameters.
* #param App\Http\Requests\StoreExample $request
*/
public function store(StoreExample $request)
{
Example::create($request->validated());
return back();
}
}
The StoreExample Form Request isn't important. It just validates and checks a gate to authorize the action.
The Observer I have set up logs this action.
# app/Observers/ExampleObserver.php
namespace App\Observers;
use App\Example;
class ExampleObserver
{
public function created(Example $example): void
{
\Log::info(auth()->id()." (".auth()->user()->full_name.") has created Example with params:\n{$example}");
}
}
The problem I have, is the way my logs depend on the auth() object to be set. Given the auth middleware and the gate it has to check in order to store an Example, there is no way a guest user will set off this code.
However, I do like to use tinker in my local and staging environments to check the behavior of the site but that can set off an error (Well, PHP notice to be more precise) because I can create Example models without being authenticated and the logger will try to fetch the property full_name from the non-object auth()->user().
So my question is as follows: Is there a way to catch when I'm specifically using the Laravel tinker session to handle my models in the Observer class?
Okay, replying to my own question: There IS a way. It requires using a Request object. Since observers do not deal with requests on their own, I injected one in the constructor. request() can be used instead, so no DI is needed.
Why is a Request important?
Because a request object has an accessible $server attribute that has the information I want. This is the relevant information I get by returning a dd($request->server) (I'm not gonna paste the whole thing. My Request's ServerBag has over 100 attributes!)
Symfony\Component\HttpFoundation\ServerBag {#37
#parameters: array:123 [
"SERVER_NAME" => "localhost"
"SERVER_PORT" => 8000
"HTTP_HOST" => "localhost:8000"
"HTTP_USER_AGENT" => "Symfony" // Relevant
"REMOTE_ADDR" => "127.0.0.1"
"SCRIPT_NAME" => "artisan" // Relevant
"SCRIPT_FILENAME" => "artisan" // Relevant
"PHP_SELF" => "artisan" // Relevant
"PATH_TRANSLATED" => "artisan" // Relevant
"argv" => array:2 [ // Relevant
0 => "artisan"
1 => "tinker"
]
"argc" => 2
]
}
So there's all these attributes I can filter by using $request->server('attribute') (returns $request->server->attribute or null, so no risk of accessing an undefined property). I can also do $request->server->has('attribute') (returns true or false)
# app/Observers/ExampleObserver.php
namespace App\Observers;
use App\Example;
class ExampleObserver
{
/* Since we can use request(), there's no need to inject a Request into the constructor
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
*/
public function created(Example $example): void
{
\Log::info($this->getUserInfo()." has created Example with params:\n{$example}");
}
private function getUserInfo(): string
{
// My logic here.
}
}
In AppServiceProvider.php, I am trying to get data from session then call API with it then pass a variable after getting it from the response.
Also, I don't know if it's right. I added "Request $request" to boot function as in other parts of code.
And the error I'm getting is "RuntimeException in Request.php line 388: Session store not set on request." Does that mean session variable isn't set? I would've thought they'd be available after I log in to my site as I session put "token" and "member_id" during login.
Is it because view controller is higher level so my session puts during login won't come before bootstrap code in boot function here uses them? Oh, or is the request not really passed in as parameter of boot function as I would've liked it to. How would I otherwise do that or get variables from the session?
Anyway, are the steps I'm taking proper? If I'm doing things incorrectly throughout, such as bad practice, please point it out as well thanks.
Here's my code:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Http\Request;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot(Request $request)
{
$client = new \GuzzleHttp\Client();
$params = array(
'token' => $request->session()->get('token'),
'member_id' => $request->session()->get('member_id'),
'activity' => 'GET MEMBER INFO'
);
$response = $client->request('POST',
env('SPACE_4_CAR_API_DOMAIN') . 'select_api/GetMemberInfo.php',
['json' => $params]
);
$returnData = json_decode($response->getBody());
view()->composer('layout', function ($view) {
$view->with('is_admin', $returnData->is_administrator);
});
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
I would like to customize the time users have to verify their email address that happens through the built in Auth (since 5.7).
In config/auth there is:
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
But I haven't found anything similar for email verification. There is also no mention in the official documentation.
Whilst the question specifically addresses Laravel 5.7, I feel that it is worth mentioning that as of Laravel 5.8, it is possible to achieve this with a config variable. My search for customising the verification expiration time returned this question as the top result, hence my addition.
If we check out Illuminate\Auth\Notifications\VerifyEmail, the verificationUrl method now looks like this:
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
['id' => $notifiable->getKey()]
);
}
As such, we can just add this block to config/auth.php to customise the time without needing to extend the classes or anything:
'verification' => [
'expire' => 525600, // One year - enter as many mintues as you would like here
],
UPDATE: I've written about the above approach, as well as another on customising the process by overiding the verificationUrl method to give you more flexibility, on my blog.
In deed the options is not there in Laravel, but since laravel makes use of the following:
a trait MustVerifyEmail (in Illuminate\Foundation\Auth\User class extended by the main User model)
Event and Notification
In the MustVerifyEmail trait, there's a method called sendEmailVerificationNotification. This is where the Notification VerifyEmail class referenced by #nakov's answer and its function verificationUrl is used:
/**
* Send the email verification notification.
*
* #return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new Notifications\VerifyEmail);
}
Since we know this, we can do the following:
Extend the Notifications\VerifyEmail to our custom VerifyEmail class
override the implementation of verificationUrl
override the implementation of the sendEmailVerificationNotification method in the User model to use our new VerifyEmail class.
Having done the above, our User model will have the following method:
/**
* Send the email verification notification.
*
* #return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new \App\Services\Verification\VerifyEmail);
}
Now we make use of our custom VerifyEmail class. Then our new VerifyEmail class would look like this:
namespace App\Services\Verification;
use Illuminate\Support\Carbon;
use \Illuminate\Support\Facades\URL;
class VerifyEmail extends \Illuminate\Auth\Notifications\VerifyEmail
{
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify', Carbon::now()->addMinute(3), ['id' => $notifiable->getKey()]
); //we use 3 minutes expiry
}
}
Well, apart from the explanations, the process is quite straight forward. I hope it is easy to grasp. Cheers!
If you open the Illuminate\Auth\Notifications\VerifyEmail::class;
The method that generates the URL already uses expiration time which defaults to 1 hour. Unfortunately there is no option to modify that value.
/**
* Get the verification URL for the given notifiable.
*
* #param mixed $notifiable
* #return string
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
);
}
In one of my applications I have a property that is needed throughout the app.
Multiple different parts of the application need access such as requests, local and global scopes but also commands.
I would like to "cache" this property for the duration of a request.
My current solution in my Game class looks like this:
/**
* Get current game set in the .env file.
* #return Game
*/
public static function current()
{
return Cache::remember('current_game', 1, function () {
static $game = null;
$id = config('app.current_game_id');
if ($game === null || $game->id !== $id) {
$game = Game::find($id);
}
return $game;
});
}
I can successfully call this using Game::current() but this solutions feels "hacky" and it will stay cached over the course of multiple requests.
I tried placing a property on the current request object but this won't be usable for the commands and seems inaccessible in the blade views and the objects (without passing the $request variable.
Another example of its usage is described below:
class Job extends Model
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::addGlobalScope('game_scope', function (Builder $builder) {
$builder->whereHas('post', function ($query) {
$query->where('game_id', Game::current()->id);
});
});
}
}
I do not believe I could easily access a request property in this boot method.
Another idea of mine would be to store the variable on a Game Facade but I failed to find any documentation on this practice.
Could you help me find a method of "caching" the Game::current() property accessible in most if not all of these cases without using a "hacky" method.
Use the global session helper like this:
// Retrieve a piece of data from the session...
$value = session('key');
// Store a piece of data in the session...
session(['key' => 'value']);
For configuration info and more options: https://laravel.com/docs/5.7/session
I have a custom validator set-up like this:
Validator::extend('valid_username', 'ProfileController#valid_username');
Then I have the following method which handles the validation. It checks both if the username already exists, and if the username contains valid characters.
public function valid_username($attribute, $value, $parameters)
{
$u = User::where('username', $value)->get();
if ($u->count())
{
// here I would like to return "Username already taken."
return FALSE;
}
else if (preg_match("/^[A-Za-z0-9#\.\-_]+$/", $value))
{
return TRUE;
}
else
{
// here I would like to return "Username contains invalid characters."
return FALSE;
}
}
I would like to alter the error message returned by this validator depending on which error caused the validation to fail. However, I don't know how to do this. In my language files I have set up the following line for the validator:
"valid_username" => "This username is already taken or contains invalid characters."
Is it possible with Laravel to return a specific error message? Or do I have to split this validation up in two custom validation rules? This might not be a problem in this case, but especially if database access is involved I would prefer to validate a retrieved Eloquent model in one validator instead of instantiating an Eloquent object twice.
After consulting the code, the answer is "not out of the box". You can, however, extend everything and make that work.
The process, which I don't have time to completely do at the moment (sorry!), would be to create a class extending Validator, making that functionality work, and then using a new ServiceProvider to replace Laravel's $app['validator'] with your own.
That process, a little more concretely, goes like this something like this:
<?php namespace MyLib\Validation;
class Validator extends \Illuminate\Validation\Validator {
// Fancy validation logic to be able to set custom messages
}
Then, you need to extend the Factory to return your new Validator:
<?php namespace MyLib\Validation;
class Factory extends \Illuminate\Validation\Factory {
// Change this method
/**
* Resolve a new Validator instance.
*
* #param array $data
* #param array $rules
* #param array $messages
* #return \MyLib\Validation\Validator
*/
protected function resolve($data, $rules, $messages)
{
if (is_null($this->resolver))
{
// THIS WILL NOW RETURN YOUR NEW SERVICE PROVIDER SINCE YOU'RE
// IN THE MyLib\Validation NAMESPACE
return new Validator($this->translator, $data, $rules, $messages);
}
else
{
return call_user_func($this->resolver, $this->translator, $data, $rules, $messages);
}
}
}
...and finally, extend the Validation service provider, use your new Factory, and then replace the default ValidationServiceProvider with your own.
<?php namespace MyLib\Validation;
class ValidationServiceProvider extends \Illuminate\Validation\ServiceProvider {
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerPresenceVerifier();
$this->app['validator'] = $this->app->share(function($app)
{
// THIS WILL NOW RETURN YOUR FACTORY SINCE YOU'RE
// IN THE MyLib\Validation NAMESPACE
$validator = new Factory($app['translator'], $app);
// The validation presence verifier is responsible for determining the existence
// of values in a given data collection, typically a relational database or
// other persistent data stores. And it is used to check for uniqueness.
if (isset($app['validation.presence']))
{
$validator->setPresenceVerifier($app['validation.presence']);
}
return $validator;
});
}
}
So anyway, that's one way to extend the Validation library with your own code. I didn't solve the issue of adding your own messages, but this will show you how, if you can read the core code and see how to add that functionality in, to go about making it work in your app.
Last note:
You may want to see how Laravel handles using Database "stuff" within validation rules - While this may not affect your application (unless it gets big!) you may want to consider using a Repository pattern of some sort and using that in your Validator::extend() call instead of the User class directly. Not necessary, just a note for something to check out.
Good luck and don't be afraid to RTFC!
Instead of making your own validation rule that does validates two things (which you shouldn't really do, validate one thing at a time), you can use the unique rule and then make your own rule that validates the characters of the username.
For example:
$rules = ['username' => 'required|username|unique:users,username'];
Where the username rule is your custom rule that ensures it contains the correct characters.
Maybe a bit "dirty" but it works:
The controller validates the input with something
$rules = array(
'title' => 'no_collision:'.$input['project_id']
);
In the validator function flash the message to the Session before returning false:
//...
public function validateNoCollision($attribute, $value, $parameters)
{
$project = Project::find($parameters[0]);
if($value == $project->title){
Session::flash('colliding_message','This collides with '.$project->title($).' created by '.$project->user->name;
return false;
}else{
return true;
}
}
In the view do something like the following:
#if($errors->has('title'))
<span class="help-block">{{ Session::get('colliding_message') }}</span>
#endif