View composer runs multiple times, how to reduce to 1 - php

I made a view composer in Laravel 5. When i use a wildcard *, to add something to all my views, it get's called at least twice. It runs when my master template is loaded, and again when my content page is included. This will give problems in the future, because it executes the query it does multiple times. I was able to fix the multiple querying by storing it in a static variable :
class StoreComposer {
static $composed;
public function __construct(StoreRepository $repository)
{
$this->store = $repository;
}
public function compose(View $view)
{
if(static::$composed)
{
return $view->with('store', static::$composed);
}
static::$composed = $this->store->pushCriteria( new WithCategories() )
->pushCriteria( new WithSettings() )
->applyCriteria()
->all();
$view->with('store', static::$composed);
}
}
My question is, is there a way to make sure it only runs once, no matter how many views i load, or is there another solution to this? The way i fixed it now doesn't feel right to me. Thanks!

Unfortunately no, there is no way to make it run once, due to the way View Composers are implemented. The Illuminate\View\View::renderContents() method is responsible for calling the composer bound to a view, and since any form of view rendering (Blade template inheritance or simple #include statements) executes that method, it means that when any view is rendered any composer bound to it gets triggered.
Since in your case you use a * wildcard to bind all views, if your page renders ten views, the composer will get executed ten times. However your approach looks like a good solution to solve this shortcoming.

You can use config here to resolve multiple times query run issue for example show below code.
public function compose(View $view)
{
if(!Config::has('composeVars'))
{
Config::set('composeVars') = [
'data' => User::all();
];
}
$view->with('*', Config::get('composeVars'));
}

Try this singleton solution or use cache https://laracasts.com/discuss/channels/laravel/executing-a-view-composer-only-once
On Laravel 5.6.38 works fine

Related

Deleting multiple files

I'm kinda stuck here, any help appreciated:
So I have a table with themes, and then sub-themes. Each theme has many sub-themes, and I want to be able to destroy a theme and all sub-themes goes along.
I did this with this controller:
public function destroy()
{
$comtheme = Theme::findOrFail(request('idDelTCom'));
$comtheme->delete();
$comfiles = Subtheme::where('comtheme_id', '=', request('idDelTCom'));
$comfiles->delete();
return back();
}
Which is working. My problem is the following:
Each Sub-theme has files associated. When I delete a single sub-theme, I can delete the file using:
unlink(storage_path('app/public/com/checklists/'.$file));
I've tried doing the same, and it won't work. I figure the problem is with my query, but I can't wrap around what I have to do here...
If I can't work this out I'll just prevent the deletion of a Theme until all sub-themes are deleted, but it would be better to just delete all.
Thanks in advance!
Use a on observer on your Theme model. There are many ways to implement this but I prefer to declare it in the model class itself, so in the class for the Theme model put this:
protected static function boot()
{
parent::boot();
static::observe(ThemeObserver::class);
}
Of course there would be a use statement pointing to your ThemeObserver class up top, place the class where ever you like in the project, with your model or a dedicated observer directory.
In the ThemeObserver class you'll want an event for deleting like this:
public function deleting(Theme $theme)
{
// delete subthemes here
}
You can set up a similar observer for your subthemes where you delete the files associated with them or just do it here.
https://laravel.com/docs/5.7/eloquent#observers
I figured it out. I know I might not have asked the question the right way, so thanks for the help anyway!
The problem was I was trying to delete several files, and my unlink code wasn't expecting a colletion or a builder obviously. I got it working like this:
public function destroy()
{
$comfiles= Comfile::where('comtheme_id', '=', request('idDelTCom'))->get();
foreach($comfiles as $comfile){
$file = $comfile->file_name;
unlink(storage_path('app/public/com/checklists/'.$file));
}
$comfileDel = Comfile::where('comtheme_id', '=', request('idDelTCom'));
$comfileDel->delete();
$comtheme= Comtheme::findOrFail(request('idDelTCom'));
$comtheme->delete();
return back();
}
So I get a collection of the data I want to delete, run a foreach loop to delete each file in there. Then I run a builder of the same data to delete it, and off we go.

How to get data, passed by a view composer to certain views, inside the controller?

My View Composer passes some data to certain views (and it works, of course):
use Illuminate\View\View;
use App\Util\Helper
class PublicSettingsComposer
{
public function compose(View $view)
{
$view->with('settings', Helper::readSettingsFromFile()); // json
}
}
The appropriate provider is added into the configuration and it provides this composer correctly for all specific views:
view()->compose('public.layouts.*', 'App\Http\ViewComposers\PublicSettingsComposer');
However, inside (only) one of my views I need specific information from database, but therefore I have to use some data, that had been passed by my View Composer:
class BranchController extends Controller
{
public function branches()
{
$settings = retrieve_settings_passed_by_PublicSettingsComposer; // This is what I need
$headquartersId = $settings->headquartersid;
return view('public.layouts.branches', [
'headquarters' => Branch::find($headquartersId) // Branch is a Model
]);
}
}
FYI: Laravel version I'm using is: 5.5
P.S. #moderators: Please, be careful with considering my question as a duplicate. I know there are many questions about view composers and passing data to the views and grabbing data from within controllers. However, I really could not find any question with this context (titles are often misleading).
I see two rather simple solutions for this. The first one is to cache the parsed file within each request. The other is to use an actual cache for this job.
The first option is very straight forward to implement. In your Helper class you'll have to introduce a static property to hold the parsed contents of the read file. Then, just like you do within the singleton pattern, you either return the cached data or you first parse the file, cache the data and return it afterwards. This scenario solves your actual problem of parsing the settings twice per requests if used in two places of your app.
class Helper
{
protected static $cachedSettings;
public function readSettingsFromFile()
{
if (!self::$cachedSettings) {
self::$cachedSettings = // Do the parsing here. This should be your current implementation of Helper::readSettingsFromFile(). You can also put this in its own function.
}
return self::$cachedSettings;
}
}
The other option is to use an actual cache (an external one). You can either cache the parsed file for a specific amount of times (like 1, 3, 5 or 10 minutes or even longer). Or you cache it forever and invalidate the cache when you update the settings (if this happens within your app and you know it was updated).
This solution does only make sense if your settings do not change too frequently though. It also depends a bit on the amount of requests you expect towards your app. If your settings change not too frequently (less than every x minutes) and your app is used frequently (multiple requests every x minutes), then it could be a viable solution.
class Helper
{
public function readSettingsFromFile()
{
return Cache::remember(function () {
$settings = // Put your current calculation here
return $settings;
}, 3 * 60); // 3 * 60 = 180 seconds
}
}

How to return once from database and store in memory in Laravel

What I want to do:
Return a bunch of rows from database, convert in a array stored in memory and make this array visible from the whole project in such a way that other controllers for example can read it. My function is simple as that:
class BoardController extends Controller
{
/*
* returns something like
* ['name' => 'description',
...
]
*
* */
public static function getAll()
{
$boards = Board::orderBy('ordem')->get();
$retorno = array();
foreach($boards as $b)
{
$retorno[$b->sigla] = $b->nome;
}
return $retorno;
}
}
If I just keep calling BoardController::getAll() it will again read from database again. I also tried making this call inside a config file into a variable and returning it there but laravel gave me a 500 error. So, what is the best practice/way to do it?
If you don't want to call the database everytime then the best approach that can be followed here is to use caching and cache the results.
The Approach is simple, You make a Database call once and cache the reaults and the next time you hit the same function you check the cache first whether its empty or not. If its not empty, then return the cached results.
Remember, the cache has a time limit otherwise if you change/update anything in the database then you'll have to clear the cache that is already stored.
Laravel has some features for caching the results. You can see it Here.
Also You can also view this link for more effective implementation of cache in Laravel.
Hope this helps.

CodeIgniter $this->session->set_userdata() on 2 simultaneous AJAX

I have simulatneous AJAX requests on codeIgniter with all of them session update like so:
function ajax1($value)
{
$this->session->set_userdata('foo', $value);
}
function ajax2($value)
{
$this->session->set_userdata('bar', $value);
}
But sometimes because of MySQL concurrency, one variable or the other is not updated, I suppose because one method gets overwrites the new value of the other method with the old value if grabbed from the db.
I cannot update the 2 sessions at the same time as they do completely different things and I don't know which ones will be called, as the page is dynamic and might have one or several of these method calls.
Anybody ran into this in the past and has a way of going around that problem?
I think this problem was fixed in the latest version of codeIgniter, but if you are still using an old one, try to replace system/libraries/Session.php with a new one, or you can just override the Session Library like follow:
application/libraries/MY_Session.php
class MY_Session extends CI_Session
{
function sess_update()
{
if (!$this->CI->input->is_ajax_request())
return parent::sess_update();
}
}

Laravel 4 - Where to call View::share

all
I know View::share should be available anywhere within my application, A common place is in my routes.php file.
I am doing this.
1) User login system, after login successfully, share something for all the views.
2) when user click other pages, can use the shared variables.
Ok, my controller.
public function index() {
$data = array(
"pageTitle" => "index",
);
$privilegeMenu = $this->privilegeApi->getUserPrivilegeByUserId(Session::get('uid'));
$topMenus = $this->menuApi->getTopMenuById($privilegeMenu);
$subMenus = $this->menuApi->getSubMenuesById($privilegeMenu);
View::share('topMenus', $topMenus);
View::share('subMenus', $subMenus);
return View::make('home.index',$data);
}
So, because I am using View share, that it is to say, in every other views, I can use topMenus and subMenus now.
But when I click other pages, I got the error: Undefined variable: topMenus.
So, I am so confused what happened? I dive into View::share source code
Laravel have a class named Environment under namespace Illuminate\View.
protected $shared = array();
public function share($key, $value = null)
{
if ( ! is_array($key)) return $this->shared[$key] = $value;
foreach ($key as $innerKey => $innerValue)
{
$this->share($innerKey, $innerValue);
}
}
public function shared($key, $default = null)
{
return array_get($this->shared, $key, $default);
}
And I found when user login successfully, topMenus shared successfully. But when I click other pages, can't topMenus in shared.
It seems everything OK, I am confused. Any one knows ?
Thanks in advanced.
This is because your View::share statement is never ran for other routes. In Laravel 4.x, there are quite a few places you could put it where it would be ran every time, but these two are most commonly used:
The easiest and most simple way is to just add it at the end of your app/start/global.php which is ran at the start of every request, for all environments.
The other way is to create a new service provider, make sure it's autoloadable and add it to your app/config/app.php's providers array.
If all you want is share a few views, I'd say it's kinda of an overkill to create a whole new class just for that. If, however, you start noticing your app/start/global.php file is getting too cluttered, I'd recommend you start splitting stuff into service providers.
PS: In Laravel 5, app/start/global.php was removed and you're left only with the second option.

Categories