Prior to me discovering how the Laravel internals work, I would define static methods for most of my services. It quickly gets difficult to manage, once you need to keep non static state for a service, or you want dynamic behavior that is determined at runtime.
At the end you'll have an Asset class that looks like this:
Asset::js();
You'll use this asset class to dynamically insert a hash of your asset files in order to cache bust your static files when you deploy.
In this post I’ll share how I defined my own Asset
class facade that is registered to the Laravel container in a service provider. First, let’s take a minute to understand my original Asset class. It was your typical static class.
<?php
namespace App\Utils;
class Asset
{
/**
* Return the asset path for the Stripe integration
*
* @return string
*/
public static function stripe()
{
if (env('APP_DEBUG', false)) {
return env('CDN_ENDPOINT') . '/js/stripe.js?t=' . time();
}
return env('CDN_ENDPOINT') . '/js/stripe.'.AMEZMO_ASSET_VERSION_HASH.'.js';
}
}
The motivation for creating a custom Asset class was to be able to call it from a view. Amezmo has an asset build pipeline that generates a 16 byte hash of the filename. This hash is then stored to be used as a URI segment for cache busting JavaScript and CSS files.
The problem with the static asset class however, is that you cannot use Laravel’s DI container to inject services, such as a configuration block, or anything else that you would need. So, for the new non static implementation.
I started off by following Laravel’s pattern of putting each provider in it’s own directory. Create a new directory under your app directory called Asset. I added AssetManagerInterface to best follow the SOLID principles. However, it isn’t necessary to always implement an interface for a simple service.
Implementing the Service Provider
The Laravel DI container is powerful. It’s especially useful for registering singelton classes, such as the Asset class or a connection Redis, for example. Lot’s of Laravel’s built-in providers are singeltons.
<?php
namespace App\Asset;
use Illuminate\Contracts\Foundation\Application;
use App\Asset\AssetManager;
class AssetServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register()
{
$this->app->singleton('amezmo.asset', function (Application $app) {
$config = $app->make('config')['app'];
return new AssetManager($app->basePath(), $config['debug'], $config['cdn']);
});
}
public function provides()
{
return ['amezmo.asset'];
}
}
It’s worth noting that you do not have to register your services directly. Laravel is smart enough to construct an instance of your service. However, since I am passing in a non-reference type as the first argument, Laravel would not be able to, simply because primitive values have no context. Once we have the service provider register to the container, we’re ready to add it to our app/config/app.php
configuration array
<?php
// Note this file is stripped down for example purposes
return [
'providers' => [
// Custom service providers are added here
App\Asset\AssetServiceProvider::class
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| 02/12/2019 - Also note, that by using an Alias, one does not have to
| use a using statement for the class
|
| For example, since Validator is in the alias list, you do not have to insert
| a use Illuminate\Support\Facades\Validator statement, you can do a
| use Validator. However, this is bad practice.
|
*/
'aliases' => [
'Asset' => App\Asset\Asset::class,
]
]
The entry added to the aliases array is that the facade Asset
can be invoked from a view. Now let’s take a look at AssetManager
where the real logic, hiding behind the facade actually takes place I’ve added other methods in this class. Perhaps it might give you some inspiration in your own projects.
As you can see, the constructor defines it’s dependencies and we provide them in the AssetServiceProvider
This is great for performance, because we will be able to store the configuration in memory, and not, for example, call env()
many times, which may end up calling into getenv from the C standard library.
<?php
namespace App\Asset;
use App\Asset\AssetManagerInterface;
class AssetManager implements AssetManagerInterface
{
/** @var string */
private $webpackAssetHash;
/**
* The assest that were compiled via Amezmo's custom generator
* @var string
*/
private $amezmoAssetHash;
/** @var bool */
private $debug;
/** @var string */
private $cdn;
public function __construct($basePath, $debug, $cdn)
{
$this->webpackAssetHash = file_get_contents($basePath . '/webpack-hash.txt');
$this->amezmoAssetHash = AMEZMO_ASSET_VERSION_HASH;
$this->debug = $debug;
$this->cdn = $cdn;
}
/**
* Return the versioned javascript asset
*
* @return string
*/
public function js()
{
return $this->cdn . '/dist/app.'.$this->webpackAssetHash.'.js';
}
/**
* Return a versioned compiled css file
*
* @return string
*/
public function css()
{
return $this->cdn . '/dist/app.'.$this->webpackAssetHash.'.css';
}
/**
* Return fully qualified asset URL, using the CDN_ENDPOINT
*
* @param string $assetPath
* @return void
*/
public function url($assetPath)
{
if ($assetPath[0] === '/') {
$assetPath = substr($assetPath, 1);
}
// With debug, we will not insert the hash, but a timestamp in order to prevent the browser
// from caching it
if ($this->debug) {
return $this->cdn . '/' . $assetPath . '?t=' . time();
}
/** @var \SplFileInfo $fileInfo **/
$fileInfo = new \SplFileInfo($assetPath);
if (in_array($fileInfo->getExtension(), ['js', 'css'])) {
return $this->cdn . '/' . $this->versionedAsset($assetPath);
}
return $this->cdn . '/' . $assetPath;
}
/**
* Return the asset path for the Stripe integration
*
* @return string
*/
public function stripe()
{
if ($this->$debug) {
return $this->cdn . '/js/stripe.js?t=' . time();
}
return $this->cdn . '/js/stripe.'.$this->amezmoAssetHash.'.js';
}
/**
* This function splits the path name in order to insert an amezmo version hash between the filename
* and the file extension
*
* x.js => x.$AMEZMO_VERSION.js
*
* @param string $asset
* @return string
*/
public function versionedAsset($asset)
{
$components = explode('.', $asset);
$componentsCount = count($components);
$buffer = '';
foreach ($components as $index => $component) {
if ($index === $componentsCount - 2) {
if ($index != 0) {
$buffer .= '.';
}
$buffer .= $component . '.' . $this->amezmoAssetHash;
} else {
if ($index != 0) {
$buffer .= '.';
}
$buffer .= $component;
}
}
return $buffer;
}
}
The Asset
facade is very simple. It defines one method called getFacadeAccessor with returns a string with the value we’ve returned from AssetServiceProvider::provides()
<?php
namespace App\Asset;
use Illuminate\Support\Facades\Facade;
class Asset extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'amezmo.asset';
}
}
Benefits of Service Provider pattern with Facades
- Being able to mock your service properly
- Not depending on any hard coded dependencies.
- All your dependencies are defined in your constructor, so you can swap in specific implementations for your use specific use case. For example, if you need different behavior for the service in a production or development environment.
This blog post was originally posted on Amezmo where they make PHP hosting and deployment painless. If you liked this guide, check out our guide on deploying Laravel applications.
Top comments (0)