Sharing Data Between Views Using Laravel View Composers
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'
);