DevK
DevK

Reputation: 9952

Laravel 5.2 view composer gets executed multiple times

I had some (major) performance issues on a project I'm working on and after logging all the queries that get executed, I realised many of them are executed multiple times and I cannot get to the root of the problem.

All the queries that get executed multiple times are in my view composer provider.

This is what my view composer looks like:

public function boot()
    {
        view()->composer('partials.sidebar', function ($view) {
            $currentCategory = $this->getCurrentCategory();
            $sidebarCategories = SidebarCategory::category($currentCategory)
                ->order()
                ->get();
            $view
                ->with('sidebarCategories', $sidebarCategories);
        });

        view()->composer('partials.footer', function ($view) {
            $footerLinks = FooterCategory::with('links.translations')->order()->get();
            $footerColumn1 = $footerLinks->filter(function ($value, $key) {
                return $value->column == 1;
            });
            $footerColumn2 = $footerLinks->filter(function ($value, $key) {
                return $value->column == 2;
            });
            $footerColumn3 = $footerLinks->filter(function ($value, $key) {
                return $value->column == 3;
            });
            $footerColumn4 = $footerLinks->filter(function ($value, $key) {
                return $value->column == 4;
            });

            $view
                ->with(compact('footerColumn1', 'footerColumn2', 'footerColumn3', 'footerColumn4'));
        });
}

Both of these queries (Sidbar and Footer categories) get executed about 6 times, even though each partials is called exactly once. They are both called in master view with @include('partialname').

I've tried this:

if($view->offsetExists('sidebarCategory'))
    return;

But the offsetExists always returns false (even after its called for 5. time).

Any idea why this is happening and what I'm doing wrong?

Edit:

I've realised where the issue is. On the page that I'm visiting (where those multiple queries get executed) there are some 404 elements (images mostly). Each time a file is not found a new 404 exception is thrown. And each time a 404 exception is thrown the 404 view gets executed => meaning footer/sidebar queries get executed as well (as they're a part of 404 view). Example: https://i.sstatic.net/dQ1jo.jpg

So the follow up question is how to prevent the view from getting rendered when there is no need to (example the 404 is an image that wasnt found).

Here is a piece of code from my routes, that I assume is the reason that this is happening:

Route::get('{slug}', ['as' => 'findBySlug', 'uses' => function($slug) {
    if(\App\DynamicCategory::findBySlug($slug)->count() > 0)
        return App::make('App\Http\Controllers\GeneralController')->getDynamicCategoryIndex($slug);
    else if(\App\DynamicPage::noCategory()->findBySlug($slug)->count() > 0)
        return App::make('App\Http\Controllers\GeneralController')->getDynamicPage($slug);
    else
        abort(404);
}]);

PS: I know this piece of code is extremely unoptimized (as it basically executes the same query twice, once to see if the item exists, another time actually in the controller). This is work in progress and it's on todo list.

Edit 2:

I've come up with the next solution, I'm open to improvements as this is a somewhat hack-ish way (if I add more folders, I will need to keep in mind to update this as well). I only have 3 direct subfolders in public folder: something, files and resources.

The solution is to check the url's first segment when rendering the exception (in file app/Exceptions/Handler.php) and return 404 response without the view if it matches one of the 3 folders:

public function render($request, Exception $e)
{
    $firstSegment = $request->segment(1);
    if(starts_with($firstSegment, 'files') || starts_with($firstSegment, 'something') || starts_with($firstSegment, 'resources')) {
        return response('Stran ne obstaja', 404);
    }

    return parent::render($request, $e);
}

Thanks in advance

Upvotes: 9

Views: 1334

Answers (3)

Dipesh Sukhia
Dipesh Sukhia

Reputation: 101

You can use config here to resolve multiple times query run issue for view compose. 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'));

}

Upvotes: 0

martindilling
martindilling

Reputation: 2929

Checking file extension

Instead of checking the url segments for the specific paths, and if you say it will only be images. So if it's only going to be needed for things with specific extensions you can check for that and return a simpler 404 response for those :)

/* \app\Exceptions\Handler.php */

public function render($request, Exception $exception)
{
    if ($exception instanceof NotFoundHttpException) {
        $ext = pathinfo($request->url(), PATHINFO_EXTENSION);
        if (in_array($ext, ['jpg', 'png'])) {
            return response('Nope');
        }
    }

    return parent::render($request, $exception);
}

Caching

Just realised it was making many requests. Maybe something like this, cache the results for 1 minutes, that should be long enough for the next 404 requests on the page to pick up the cache instead of running the queries :)

public function boot()
{
    // Sidebar
    $sidebarCategories = Cache::store('file')->remember('sidebarCategories', 1, function () {
        $currentCategory = $this->getCurrentCategory();
        return SidebarCategory::category($currentCategory)->order()->get();
    });

    view()->composer('partials.sidebar', function ($view) use ($sidebarCategories) {
        $view->with('sidebarCategories', $sidebarCategories);
    });


    // Footer
    $footerColumns = Cache::store('file')->remember('footerColumns', 1, function () {
        $footerLinks = FooterCategory::with('links.translations')->order()->get();
        return [
            1 => $footerLinks->filter(function ($value, $key) { return $value->column == 1; }),
            2 => $footerLinks->filter(function ($value, $key) { return $value->column == 2; }),
            3 => $footerLinks->filter(function ($value, $key) { return $value->column == 3; }),
            4 => $footerLinks->filter(function ($value, $key) { return $value->column == 4; }),
        ];
    });

    view()->composer('partials.footer', function ($view) use ($footerColumns) {
        $view->with([
            'footerColumn1' => $footerColumns[1],
            'footerColumn2' => $footerColumns[2],
            'footerColumn3' => $footerColumns[3],
            'footerColumn4' => $footerColumns[4],
        ]);
    });
}

Upvotes: 1

Tschallacka
Tschallacka

Reputation: 28722

Don't route file requests through laravel and serve a blank image on 404

.htaccess sample:

RewriteEngine On
RewriteCond %{REQUEST_URI} \.(jpg|jpeg|gif|png|ico)$ [NC]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule .*$ /no_picture.png [L]

You would post this above the rules that redirect to index.php

This is assuming that pictures are served from the website and not by streaming database contents.

Basically what happens is this:

  • Backend user uploads image:
  • laravel stores image in /some/path/to/store/data/public/12a/4d8/458/12a4d8458.gif
  • laravel stores image as 12a4d8458.gif in your database
  • -------------------- time passes ------------------------
  • visitor requests page.
  • request doesn't match a file. redirect to index.php(as per .htaccess)
  • request gets send to index.php
  • laravel builds page. Finds image
  • laravel composes full public url path
  • www.awesome.com/data/public/12a/4d8/458/12a4d8458.gif
  • composed html contents get pushed to visitor
  • ---------------- milliseconds pass ------------------------
  • visitor requests image /data/public/12a/4d8/458/12a4d8458.gif
  • request matches file file gets served by apache
  • laravel remains blissfully unaware of request
  • ----------------- not a millisecond has passed -------------
  • visitor requests image /data/public/4e8/d44/98f/4e8d4498f.gif
  • request doesn't match a file. redirect to index.php(as per .htaccess)
  • laravel builds page. doesn't find a route.
  • laravel invokes 404 routines
  • laravel builds 404 page with all triggers associated
  • laravel serves 404 to user

So what you want to do in the last step is prevent the image request even reaching laravel, by serving your own image as per defined by .htaccess rules. That way your webserver can respond much faster on the missing image.

Upvotes: 5

Related Questions