Sharing Data Between Views Using Laravel View Composers

Sharing data between views using Laravel View Composer

Getting Started With Laravel Views

Views hold the presentation logic of a Laravel application. It is served separately from the application logic using laravel's blade templating engine.

Passing data from a controller to a view is as simple as declaring a variable and adding it as a parameter to the returned view helper method. There is no shortage of ways to do this.

We will create a SampleController class that will handle our logic

php artisan make:controller SampleController

Here is a sample controller in app/Http/Controllers/SampleController.php

class SampleController extends Controller
{
    /**
     * pass an array to the 'foo' view
     * as a second parameter.
    */
    public function foo()
    {
        return view('foo', [
            'key' => 'The big brown fox jumped over the lazy dog'
        ]);
    }

    /**
     * Pass a key variable to the 'foo view
     * using the compact method as
     * the second parameter.
    */
    public function bar()
    {
        $key = 'If a would chuck can chuck wood,';

        return view('foo', compact('key'));
    }

    /**
     * Pass a key, value pair to the view
     * using the with method.
    */
    public function baz()
    {
        return view('foo')->with(
            'key',
            'How much woood would a woodchuck chuck.'
        );
    }
}

Passing Data To Multiple Views

This is all fine and dandy. Well it is until you try passing data to many views.

More often than not, we need to get some data on different pages. One such scenario would be information on the navigation bar or footer that we be available across all pages on your website, say, the most recent movie in theatres.

For this example, we will use an array of 5 movies to display the latest movie (the last item on the array) on the navigation bar.

For this, I will use a boostrap template to setup the navigation bar in resources/views/app.blade.php.

<nav class="navbar navbar-inverse">
        <div class="container-fluid">

            <!-- Brand and toggle get grouped for better mobile display -->
            <div class="navbar-header">
                <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a class="navbar-brand" href="/">Movie Maniac</a>
            </div>

            <!-- Collect the nav links, forms, and other content for toggling -->
            <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
                <ul class="nav navbar-nav">
                    <li><a href="foo">Foo</a></li>
                    <li><a href="bar">Bar</a></li>
                    <li><a href="baz">Baz</a></li>
                </ul>

                <ul class="nav navbar-nav navbar-right">
                    <li><a href="#">latest movie title here</a></li>
                </ul>

            </div>
            <!-- /.navbar-collapse -->
        </div>
        <!-- /.container-fluid -->
    </nav>

Placeholder text for latest movie on navbar

The latest movie text on the far right will however be replaced with a title from our movie list to be created later on.

Let's go ahead and create our movie list on the homepage.

Routes

View all the routes in app/Http/routes.php

Route::get('/', 'SampleController@index');

Route::get('/foo', 'SampleController@foo');

Route::get('/bar', 'SampleController@bar');

Route::get('/baz', 'SampleController@baz');

We are just creating four simple routes.

Controller

View the controller in app/Http/Controllers/SampleController.php

    /**
     * Return a list of the latest movies to the
     * homepage
     *
     * @return View
     */
    public function index()
    {
        $movieList = [
            'Shawshank redemption',
            'Forrest Gump',
            'The Matrix',
            'Pirates of the Carribean',
            'Back to the future',
        ];

        return view('welcome', compact('movieList'));
    }

View

See latest movie views in resources/views/welcome.blade.php

@extends('app')

@section('content')
        <h1>Latest Movies</h1>
        <ul>
        @foreach($movieList as $movie)
            <li class="list-group-item"><h5>{{ $movie }}</h5></li>
        @endforeach
        </ul>
@endsection

It goes without saying that my idea of latest movies is skewed, but we can overlook that for now. Here is what our homepage looks like now.

Homepage with list of latest movies

Awesome! We have our movie list. And now to the business of the day.

Update Index Controller Method

We will assume that Back to the future, being the last movie on our movie list, is the latest movie, and display it as such on the navigation bar.

    /**
     * Return a list of the latest movies to the
     * homepage
     *
     * @return View
     */
    public function index()
    {
        $movieList = [
            'Shawshank redemption',
            'Forrest Gump',
            'The Matrix',
            'Pirates of the Carribean',
            'Back to the future',
        ];

        $latestMovie = end($movieList);

        return view('welcome', compact('movieList', 'latestMovie'));
    }

Error In Shared View File

We now have Back to the future as our latest movie, and rightfully so because Back to the Future 4 was released a week from now in 1985. I cannot make this stuff up.

Homepage with latest movie on nav bar

This seems to work. Well until you try navigating to other pages (Try one of foo, bar, baz) created earlier on. This will throw an error.

Error due to missing latest movie on nav bar

Fixing Error in Shared View

This was expected and by now you must have figured out why this happened. We declared the latest movie variable on the index method of the controller and passed it to the welcome biew. By extension, we made latestMovie available to the navigation bar BUT only to views/welcome.blade.php.

When we navigate to /foo, our navigation bar still expects a latestMovie variable to be passed to it from the foo method but we have none to give.

There are three ways to fix this

  • Declare the latestMovie value in every other method, and in this case, the movieList too. It goes without saying we will not be doing this.

  • Place the movie information in a service provider's boot method. You can place it on App/Providers/AppServiceProvider or create one. This quickly becomes inefficient if we are sharing alot of data.
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        view()->share('key', 'value');
    }
  • Take advantage of Laravel View composers

Enter Left: Laravel View Composers

View composers are callbacks or class methods that are called when a view is rendered. If you have data that you want to be bound to a view each time that view is rendered, a view composer can help you organize that logic into a single location.

-Laravel documentation

While it is possible to get the data in every controller method and pass it to the single view, this approach may be undesirable.

View composers, as described from the laravel documentation, bind data to a view every time it is rendered. They clean our code by getting fetching data once and passing it to the view.

Creating A New Service Provider

Since Laravel does not include a ViewComposers directory in it's application structure, we will have to create our own for better organization. Go ahead and create App\Http\ViewComposers

We will then proceed to create a new service provider to handle all our view composers using the artisan command

php artisan make:provider ComposerServiceProvider

The service provider will be visible in app/Providers

Add the ComposerServiceProvider class to config/app.php array for providers so that laravel is able to identify it.

Modify the boot method in the new Provider by adding a composer method that extends view()

/**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        view()->composer(
            'app',
            'App\Http\ViewComposers\MovieComposer'
        );
    }

Laravel will execute a MovieComposer@compose method every time app.blade.php is rendered. This means that every time our navigation bar is loaded, we will be ready to serve it with the latest movie from our view composer.

While MovieComposer@compose is the default method to be called, you can overwrite it by specifying your own custom method name in the boot method.

view()->composer('app',  'App\Http\ViewComposers\MovieComposer@foobar');

Creating MovieComposer

Next, we will create our MovieComposer class

<?php

namespace App\Http\ViewComposers;

use Illuminate\View\View;

class MovieComposer
{
    public $movieList = [];
    /**
     * Create a movie composer.
     *
     * @return void
     */
    public function __construct()
    {
        $this->movieList = [
            'Shawshank redemption',
            'Forrest Gump',
            'The Matrix',
            'Pirates of the Carribean',
            'Back to the future',
        ];
    }

    /**
     * Bind data to the view.
     *
     * @param  View  $view
     * @return void
     */
    public function compose(View $view)
    {
        $view->with('latestMovie', end($this->movieList));
    }
}

The with method will bind the latestMovies to the app view when it is rendered. Notice the we added Illuminate\View\View .

Cleaning Up The Controller with MovieComposer

We can now get rid of the latestMovie variable and actually remove it from the compact helper method in SampleController@index.

public function index()
    {
        $movieList = [
            'Shawshank redemption',
            'Forrest Gump',
            'The Matrix',
            'Pirates of the Carribean',
            'Back to the future',
        ];

        return view('welcome', compact('movieList'));
    }

We can now access the latest movie on the navigation bar in all our routes.

View latest movies in all routes

Optimizing Our Code With A Repository

While we seem to have solved one issue, we seem to have created another. we now have two movieList arrays. one in the controller and one in the constructor method in MovieComposer.

While this may have been caused by using an array as a simple data source, it may be a good idea to fix it, say, by creating a movie repository. Ideally, the latestMovie value would be gotten from a database using Eloquent.

Check out the github repo for this tutorial to see how I created a Movie Repository to get around the redudancy as shown below in MovieComposer and SampleController.

    public $movieList = [];

    /**
     * Create a movie composer.
     *
     *  @param MovieRepository $movie
     *
     * @return void
     */
    public function __construct(MovieRepository $movies)
    {
        $this->movieList = $movies->getMovieList();
    }

    /**
     * Bind data to the view.
     *
     * @param  View  $view
     * @return void
     */
    public function compose(View $view)
    {
        $view->with('latestMovie', end($this->movieList));
    }
    public function index(MovieRepository $movies)
    {
        $movieList = $movies->getMovieList();

        return view('welcome', compact('movieList'));
    }

Conclusion

It is possible to create a view composer that is executed when all views are rendered by replacing the view name with an asterisk wildcard

view()->composer('*', function (View $view) {
    //logic goes here
});

Notice that instead of passing a string with the path to MovieComposer, you can also pass a closure.

You can as also limit the view composer to a finite number of views, say, nav and footer

view()->composer(
    ['nav', 'footer'],
    'App\Http\ViewComposers\MovieComposer'
);

John Kariuki

Software developer at Andela. Proficient in PHP with Laravel and Codeigniter.

Conversant with MEAN(MongoDB, Express.js, AngularJS, Node.js) and currently learning Python and Go.

Avid blog reader and fascinated by drones.

I play basketball, swim and jog in my free time.