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
Related
I am getting the following exception when trying to use cache remember:
But in the Laravel docs (https://laravel.com/docs/5.7/cache) they are using a closure in their example:
Any help much appreciated ... have googled it and people seem to be having issue with forever() closures not being serializable (but suggested solutions not working for me)
/**
* #param string $guid
* #return Account
*/
public function getAccount(string $guid): Account
{
$key = md5(sprintf('xero/accounts[guid="%s"]', $guid));
return $this->cache->remember($key, Carbon::now()->addHour(), function () use ($guid) {
return $this->xero->loadByGUID(Account::class, $guid);
});
}
I've now also tried doing this instead (to get around passing a closure to cache::remember fxn):
public function getAccount(string $guid): Account
{
$key = md5(sprintf('xero/accounts[guid="%s"]', $guid));
$account = $this->cache->get($key);
if ($account === null) {
//dump('account not found, storing in cache...');
/** #var Account $account */
$account = $this->xero->loadByGUID(Account::class, $guid);
$this->cache->put($key, $account, Carbon::now()->addHour());
}
}
But still getting same error (cannot serialize Closure) at the line '$this->cache->put($key, $account, Carbon::now()->addHour());'
The $account object is of type: use XeroPHP\Models\Accounting\Account;
(from https://github.com/calcinai/xero-php)
Models contain a reference to the Xero Application which contain an instance of a Guzzle client, which itself has properties that contains closures.
PHPs serialize function is not able to serialize Closures: https://3v4l.org/1MIpd
A possibility would be calling toStringArray and fromStringArray when you store and retrieve a model.
(full credit to Josh-G: https://github.com/calcinai/xero-php/issues/734)
I have the following problem. I hope my approach is not completely wrong, feel free to advice.
I have a Model class Chat.php
protected $skip;
protected $take;
protected $agreements;
protected $chat;
public function getSkip()
{
return $this->skip;
}
public function setSkip($skip)
{
$this->skip = $skip;
}
public function getTake()
{
return $this->take;
}
public function setTake($take)
{
$this->take = $take;
}
public function __construct(array $attributes = array())
{
parent::__construct($attributes);
$this->setTake(8);
$this->setSkip(8);
}
I set properties skip and take here.
Then, I have the DashboardController
class DashboardController extends Controller
{
private $chat;
/**
* DashboardController constructor.
* #param $chat
*/
public function __construct(Chat $chat)
{
$this->chat = $chat;
}
/**
* Display a listing of the authenticated resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$chats = Chat::orderBy('created_at','desc')->skip($this->chat->getSkip())->take($this->chat->getTake())->get();
$agreements = AgrType::orderBy('created_at','desc')->take(10)->get();
return view('sections.dashboard', compact('chats','agreements'));
}
public function loadMore()
{
$this->chat->setSkip($this->chat->getSkip() - 1 );
$this->chat->setTake($this->chat->getTake() - 1);
return redirect('/dashboard');
}
My approach is as follows:
After a user clicks on button, route /loadmore get him to this controller and triggers loadMore function.
LoadMore function then gets values through accessors and sets values with mutator.
Index method then simply reads this values.
So the meaning is: I show chat window (rather maybe comments window cause this si not really a chat), index method is called.
Index method reads the values and displays comments according to query. -> this one is OK
Now, what does NOT work:
When I click button, loadMore function gets called, sets the values which index method then reads and reloads according to them.
What did I try: I tried loadMore method to display its own values (changed) and return them, but then I have a new route for reloaded chat and it is not what I want.
What do I miss? Is my approach OK? (I know javascript is maybe better for this, but I want a Laravel way, get and post.)
Thanks in advance.
Your controller functions get executed within in completely separate requests.
All return redirect('/dashboard'); does in loadMore() send your browser to the dashboard route. Your browser then makes a new request to index() on which your controller gets instantiated again, its __construct() function run again and a new empty Chat model gets instantiated.
I recommend you put the take and skip parameters into your url like this:
$router->get('/dashboard/{skip}/{take}', 'DashboardController#index');
And change your index() function to this:
public function index($skip, $take)
That way it will work, however the even better way of doing it would be to use Laravel's paginate() function: https://laravel.com/docs/5.6/pagination
public function index()
{
$chats = Chat::orderBy('created_at','desc')->paginate();
$agreements = AgrType::orderBy('created_at','desc')->take(10)->get();
return view('sections.dashboard', compact('chats','agreements'));
}
I have a system in laravel 5.3 that uses over 40 controllers and probably 200 views.
I am attempting to clean up the code and use best practice. Given that certain calls are made pretty much everywhere, it makes sense to define it somewhere "semi globally". I would assume this would be in the Controller from which all controllers extend.
One object is $user, and has child $user->organisations and $user->organisation->locations.
Loading this at a base controller (or equivalent) way would also give me the advantage that I could ensure child relationships were eager loaded in an optimal way ensuring any foreach style code never results in multiple small database lookups. There are a number of other items I want to do this for with similar ramifications for database optimisation. These all use the Auth::user(), and they affect permissions with child objects.
Given that there are about 20 properties/variables of use to be shared (all dependant on Auth::user()) removing this duplicated code from almost every method is a huge improvement.
My aim is to be able to reference $this->user from any controller, and already have pre-loaded all the child/related objects.
Laravel 5.3 re-organised the loading order, so sharing the logged-in user data as part of Controller::__construct is no longer feasible.
Here's the code attempted so far:
In Controller::__construct
$this->middleware(function ($request, $next) {
$this->user = Auth::user();
view()->share('user', $this->user);
return $next($request);
});
Unsurprisingly, this correctly sets the $user variable at View level, but not Controller level. While I do $user it at view level, this doesn't help.
Given that there are about 20 properties/variables of use to be shared (all dependant on Auth::user()) I decided that one Helper would at least move this into a centralised location. I instantiate the helper to be stored as property across all controllers : $this->authentication_helper
In an example controller: SearchController :
public function index(Request $request): View
{
$this->authentication_helper->getAuthenticationData($this);
//... logic for the search
}
With the AuthenticationHelper doing (amongst other things):
public function getAuthenticationData(Controller $controller) : void
{
$user = Auth::user();
$controller->user = User::with(
organisations.locations', // .. other children .. //
)->find($user->id);
// share to the view
View::share('user', $controller->user);
// ... other $controller property setting
return;
}
I am unsure as to whether this is best practice.
edit - A previously raised second issue been solved - the main question remains:
Is there a problem with this approach - what would be an equivalent way of moving these 20 or so variable assignments to a higher level.
You can get authorized user in controller using Laravel DI, simply your code should be something like this
YourControlle extends Controller
{
public function test(Request $request)
$user = $requset->user(); //use auth user
}
The best approach would be a helper (or multiple helpers, you can create a app\Helpers namespace for that) and have all your logic within it.
The Auth::user() will be accessible from that helper using the Auth Facade, and have your logic there.
An other simpler way is just extending your controllers from a custom base controller you make (which you're going to extend from Controller) and append a $user member to it with a protected visibility, and share it to the view, to be done with in the constructor or in a method to be called through parent::magicMethod()
EDIT
You can override the callAction method used by the controller class
/**
* Execute an action on the controller.
*
* #param string $method
* #param array $parameters
* #return \Symfony\Component\HttpFoundation\Response
*/
public function callAction($method, $parameters)
{
// insert your logic here
return call_user_func_array([$this, $method], $parameters);
}
The reason why you can't access the Auth via the constructor is because the session was not fired up yet. You can catch it when it does with this event listener :
Event::listen(Authenticated::class, function ($event) {
$this->user = $event->user;
});
not tested
A middleware with a closure would do the work too. Make it have your logic, and use it in all your controllers.
A common setup in Laravel routing is to use nested resources with route model binding. This allows great logical urls that represent the actual relationships that the models have with each other in the database. An example of this might be /library/section/book/. The book is owned by the section, the section is owned by the library. But when using route model binding, the ids of these resources are turned into models without any knowledge of each other. /1/7/234 would return the models of these resources but there is no guarantee that they are properly related. book 234 might not be owned by section 7 and section 7 might not be owned by library 1. I often have a method at the top of each controller that handles checking what I call relationship tests. This function would be found in the Book controller.
private function relationshipCheck($library, $section, $book)
{
if(library->id == $section->library_id) {
if($book != false) {
if($section->id == $book->section_id) {
return true;
} else {
return response()->json(["code" => 401], 401);
}
} else {
return true;
}
} else {
return response()->json(["code" => 401, 401);
}
}
What is the proper way to handle using these sorts of routes that represent relationships? Is there a more automated way to do this? Is there a good reason to just ignore everything but the last resource when the relationships are all one to many?
It's an old question, but still relevant today. There is a good answer here, which suggests explicitly binding the models in question. It's similar to another answer here, but with less abstraction.
Route::bind('section', function ($section, $route) {
return Section::where('library_id', $route->parameter('library'))->findOrFail($section);
});
Route::bind('book', function ($book, $route) {
return Book::where('Section_id', $route->parameter('section'))->findOrFail($book);
});
This will automatically work everywhere. If required, you could test for the upstream parameter to be found, and only perform the test in those cases (e.g. to cater for routes where only a book is specified).
Route::bind('book', function ($book, $route) {
$section = $route->parameter('section');
return $section ? Book::where('Section_id', $route->parameter('section'))->findOrFail($book) : $book;
});
...when using route model binding, the ids of these resources are turned into models without any knowledge of each other.
I am just starting to deal with this and here is how I've decided to make the approach.
Make it easier to check a model's relations
Laravel 5.3 has a method to determine if two models have the same ID and belong to the same table. is()
I submitted a pull request that would add relationship tools. You can see the changes to Illuminate\Database\Eloquent\Model that I am using in my project.
Create a middleware for nested routes with model binding.
Middleware
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Exception\HttpResponseException;
/**
* Class EntityChain
*
* Determine if bound models for the route are related to
* each other in the order they are nested.
*
* #package App\Http\Middleware
*/
class EntityChain
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
// Array of the bound models for the route.
$parameters = array_filter($request->route()->parameters(),
function ($v) {
if ($v instanceof Model) return true;
return false;
});
// When there are two or more bound models.
if (count($parameters) > 1) {
// The first model is the parent.
$parent = array_shift($parameters);
while (count($parameters) > 0) {
// Assume the models are not related.
$pass = false;
// Set the child model.
$child = array_shift($parameters);
// Check if the parent model is related to the child.
if ($parent->is_related($child)) {
$pass = true;
}
$parent = $child;
// Fail on no relation.
if (!$pass) {
throw new HttpResponseException(response()->json('Invalid resource relation chain given.', 406));
}
}
}
return $next($request);
}
}
I've come across the need to do this before. This is how I've done it:
In my RouteServiceProvider.php file I have the following method:
private function addSlugBindingWithDependency(Router $router, $binding, $className, $dependency, $dependencyClassName, $dependencyField)
{
$router->bind($binding, function($slug, $route) use($className, $dependency, $dependencyClassName, $dependencyField) {
if (!is_string($slug)) {
throw new NotFoundHttpException;
}
$params = $route->parameters();
if (!$params || !isset($params[$dependency]) || get_class($params[$dependency]) != $dependencyClassName) {
throw new NotFoundHttpException;
}
$dependencyInstance = $params[$dependency];
$item = $className::where('slug', $slug)->where($dependencyField, $dependencyInstance->id)->first();
if (!$item) {
throw new NotFoundHttpException;
}
return $item;
});
}
It's a function that helps me set up a route/model binding for a slug, where that slug depends on another part of the URL/path. It works by taking a look at the already bound parts of the route and grabbing the instance of the model it had previously bound and uses it to check that the two are linked together.
I also have another, more basic helper function, addSlugBinding that I use to bind a slug to an object too.
You would use it in the boot method of the RouteServiceProvider class like this:
public function boot(Router $router)
{
parent::boot($router);
$this->addSlugBinding($router, 'librarySlug', 'App\Library');
$this->addSlugBindingWithDependency($router, 'sectionSlug', 'App\Section', 'librarySlug', 'App\Library', 'library_id');
$this->addSlugBindingWithDependency($router, 'bookSlug', 'App\Book', 'sectionSlug', 'App\Section', 'section_id');
}
Then in my routes file I might have the following:
Route::get('{librarySlug}/{sectionSlug}/{bookSlug}', function($librarySlug, $sectionSlug, $bookSlug) {
});
Note: I've done this when I've wanted nested URLs by slug rather than ID, but it can easily be adapted to use IDs.
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.