Use Laravel Contracts to Build a Laravel 5 Twig Package
Laravel 5 is finally out, and with all the awesome features it brings. One of the new architectural changes is the new Contracts Package. In this article we are going to understand the reasoning behind this change and try to build a practical use case using the new Contracts.
What Is a Contract
A contract is an interface that defines a behaviour. Let’s take this definition from Wikipedia.
In object-oriented languages, the term interface is often used to define an abstract type that contains no data or code, but defines behaviors as method signatures.
With that being said, we use interfaces to extract the object behaviour from a class to an interface and only depend upon the definition. In Laravel’s IoC container you can bind an interface to an implementation.
$this->app->bind('App\Contracts\EventPusher', 'App\Services\PusherEventPusher');
Now every time you resolve the EventPusher
interface from the container, you will receive a Pusher
implementation. If you decide to switch to another service like Fanout, you only need to change the binding.
$this->app->bind('App\Contracts\EventPusher', 'App\Services\FanoutEventPusher');
Most of Laravel core services now are extracted to a contract, and if you want for example to override the Illuminate/Mail
service, you can implement the Illuminate\Contracts\Mail
contract and define the necessary methods and add it a s a provider.
Laravel View Contract
Laravel uses its own template engine called Blade. However, I want to use Symfony Twig as my template engine. Blade already offers the possibility to register your own extensions; check the Twig Bridge for more info. Laravel 5 has a better way to achieve the same goal using Contracts, so let’s create our own implementation.
Defining The Package
To start building our package, we need to define it inside the composer.json
file. We require Twig and autoload our src
folder as our root namespace.
// composer.json
{
"name": "whyounes/laravel5-twig",
"description": "Twig for Laravel 5",
"authors": [
{
"name": "RAFIE Younes",
"email": "younes.rafie@gmail.com"
}
],
"require": {
"twig/twig": "1.18.*"
},
"autoload": {
"psr-0": {
"RAFIE\\": "src/"
}
}
}
View Service Provider
The preferable way to register your package bindings is through a Service Provider.
// src/RAFIE/Twig/TwigViewServiceProvider.php
public function registerLoader()
{
$this->app->singleton('twig.loader', function ($app) {
$view_paths = $app['config']['view.paths'];
$loader = new \Twig_Loader_Filesystem($view_paths);
return $loader;
});
}
The registerLoader
method will bind our Twig Loader to the container. The $app['config']['view.paths']
contains our view paths. By default, it has only the resources/views
folder.
// src/RAFIE/Twig/TwigViewServiceProvider.php
public function registerTwig()
{
$this->app->singleton('twig', function ($app) {
$options = [
'debug' => $app['config']['app.debug'],
'cache' => $app['config']['view.compiled'],
];
$twig = new \Twig_Environment($app['twig.loader'], $options);
// register Twig Extensions
$twig->addExtension(new \Twig_Extension_Debug());
// register Twig globals
$twig->addGlobal('app', $app);
return $twig;
});
}
The Twig_Environment
class is the Twig core class. It accepts a Twig_LoaderInterface
and a list of options:
$app['config']['app.debug']
retrieves our debug flag from the config file.$app['config']['view.compiled']
is our compiled views path registered inside theconfig/view.php
file.
The $twig->addGlobal
method registers a variable to be available for all views.
// src/RAFIE/Twig/TwigViewServiceProvider.php
namespace RAFIE\Twig;
class TwigViewServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerLoader();
$this->registerTwig();
$this->app->bind('view', function ($app) {
return new TwigFactory($app);
});
}
}
The register
method will bind the twig
and twig.loader
to the container. The view
key was previously holding the Blade view factory, now it will resolve our new TwigFactory
class which will be responsible for rendering our view.
Laravel won’t load your service provider by default, so you need to register it inside your config/app.php
providers array. We will also comment out the Laravel view service provider.
// config/app.php
...
'providers' => [
'RAFIE\Twig\TwigViewServiceProvider',
//'Illuminate\View\ViewServiceProvider',
...
View Factory
The TwigFactory
class must implement the Illuminate\Contracts\View\Factory
interface to get the shape and behaviour of the view system. This class will do the job of passing the view to the Twig parser. To achieve more loose coupling, we have a TwigView
class which implements the Illuminate\Contracts\View\View
contract. This class will behave as a bag for the view object, and will have a reference to the TwigFactory
class.
// src/RAFIE/Twig/TwigFactory.php
class TwigFactory implements FactoryContract
{
/*
* Twig environment
*
* @var Twig_Environment
* */
private $twig;
public function __construct(Application $app)
{
$this->twig = $app['twig'];
}
public function exists($view)
{
return $this->twig->getLoader()->exists($view);
}
public function file($path, $data = [], $mergeData = [])
{
// or maybe use the String loader
if (!file_exists($path)) {
return false;
}
$filePath = dirname($path);
$fileName = basename($path);
$this->twig->getLoader()->addPath($filePath);
return new TwigView($this, $fileName, $data);
}
public function make($view, $data = [], $mergeData = [])
{
$data = array_merge($mergeData, $data);
return new TwigView($this, $view, $data);
}
public function share($key, $value = null)
{
$this->twig->addGlobal($key, $value);
}
public function render($view, $data)
{
return $this->twig->render($view, $data);
}
public function composer($views, $callback, $priority = null){}
public function creator($views, $callback){}
public function addNamespace($namespace, $hints){}
}
We resolve the twig
object from the container with the parameters explained previously and we start implementing method’s logic. I omitted the last three functions, because defining them will involve creating events and dispatching them, and will make our simple example more complex. The make
method returns a new TwigView
instance with the current factory, view, and data.
// src/RAFIE/Twig/TwigView.php
use Illuminate\Contracts\View\View as ViewContract;
class TwigView implements ViewContract
{
/*
* View name to render
* @var string
* */
private $view;
/*
* Data to pass to the view
* @var array
* */
private $data;
/*
* Twig factory
* @var RAFIE\Twig\TwigFactory
* */
private $factory;
public function __construct(TwigFactory $factory, $view, $data = [])
{
$this->factory = $factory;
$this->view = $view;
$this->data = $data;
}
public function render()
{
return $this->factory->render($this->view, $this->data);
}
public function name()
{
return $this->view;
}
public function with($key, $value = null)
{
$this->data[$key] = $value;
return $this;
}
public function __toString()
{
return $this->render();
}
}
Let’s take a normal scenario where we have our index page route that returns a home view, and we also provide a variable that ensures our data is passed to the view.
// app/Http/routes.php
Route::get('/', function(){
return View::make('home.twig', ['name' => 'younes']);
});
// we can also pass the name by chaining the call to the `with` method.
Route::get('/', function(){
return View::make('home.twig')->with('name', 'younes');
});
// resources/views/home.twig
<h2>Hello {{ name|upper }}</h2>
When you hit your application’s index page, the execution flow will be:
- The
View
facade will be resolved from the container and return aTwigFactory
instance. - The
make
method is called and it returns a newTwigView
instance with the factory, view and data. - Laravel
Router
accepts aResponse
object or a string as a request response, so the__toString
method is called on ourTwigView
instance. Therender
method inside theTwigFactory
calls therender
method on theTwig_Environment
object.
When using contracts, the application API is always consistent, so testing the other implemented methods will be done the same way as before.
// test if a view exists
View::exists('contact.twig');
// render a view from a different folder
View::file('app/contact.twig');
Conclusion
Check out the final version of the project here.
The new Contracts package provides a better way to extend Laravel core components, and provides a stable API for developers to access the framework’s behaviour. Let me know what you think about the new Contracts package in the comments, and if you have any questions don’t hesitate to post them below.