My Laravel Package Building Workflow
- CMS & FrameworksDebugging & DeploymentDesign PatternsDevelopment EnvironmentFrameworksLaravelPatterns & Practices
This article was peer reviewed by Claudio Ribeiro. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!
Packages are a really important part of the Laravel experience (just like with any other framework). Whatever we need to do, there’s probably already a package for it out there; ready for a composer require
to bring some magic in.
Some weeks ago, I had an idea for a new one. I work for AdEspresso, where we have a Symfony FeatureBundle, which handles feature toggling in our projects. It’s a really nice piece of code that we use to release new features only for specific subsets of users. So, I asked myself… why not port it for Laravel? That’s how my Laravel Feature package idea was born.
Before getting started, I had to ask myself another question: which path should I follow to make a package for Laravel 5? I’ve found tons of tips on the internet, many guidelines, but not a real methodology about how I can build a package for my favorite framework. In this article, I will try to explain how I prepared my development environment for it, and which choices I made when building the package. Not the development of every line of code per-se, but the actual workflow of getting from nothing to a full package.
Obviously, don’t take every step for granted: I am fully aware that something can be sloppy or plain wrong. Feel free to give me some feedback if you want by leaving a comment below the article!
Feature flagging?
When we implement feature flagging in our software, we have the ability to control when to roll-out a specific feature, and also who should be able to use that feature – very useful for big projects, less so for small ones.
A typical example of a feature flagging application is the concept of a “canary release” (canaries were used in mines to detect toxic gas, because they got knocked out by it before humans did – they were “testers”). Imagine wanting to deploy a new feature: however, we want to restrict access to it in the first week for better monitoring, in order to better understand what happened if something goes wrong. We have a specific set of users (e.g. 5% of our total user base) identified as testers, whom we “use” for this experiment. Implementing feature flagging means more control over our software feature’s lifecycle and avoiding bugs and errors.
Laravel does not offer anything out of the box for feature flagging. The authorization component is probably something similar to it: however, it’s always related to a specific resource. Definitely not our case: a feature could involve more than one resource at once, or sometimes none! So… let’s make one ourselves!
Note that I am not going to cover everything about the package itself. What I want to do is to explain the workflow I used to build the package in a more general way.
That said… To the package machine!
Preparing the Environment
I always use Homestead Improved in my projects, and you should do the same. In a matter of minutes, we’ll have a new VM up and running for our experiments! If we are really, really lazy, we could try LaraPrep, a useful script that prepares everything. It’s only compatible with Linux right now, but it should also work on your Mac.
This article will cover the 5.4 version of the framework.
Namespacing and Folders
The first question that needs an answer when developing a new package is: “where to put my code?”
It’s generally recommended to use PSR-4 autoloading to create another namespace, separated from the project code, and to “link” it to a dedicated folder.
The default value for the item in composer.json
file is
"psr-4": {
"App\\": "app/"
}
all we have to do here is to add a new item like
"psr-4": {
"App\\": "app/",
"LaravelFeature\\": "LaravelFeature/src"
}
Assuming that the LaravelFeature
folder will contain the package code, we are associating it with a totally separated namespace. This degree of separation is a good start, but obviously it’s not enough. Let’s look into structure some more.
Playing with the Right Skeleton
If you take a look at the code on GitHub, you will notice various files like CHANGELOG.md
, CONTRIBUTING.md
and many others. I didn’t invent them: they are common practice, and suggested by the PHPLeague Skeleton Package, a fantastic boilerplate for anyone who wants to develop a new PHP package.
Let’s see its most important parts:
- the
src
folder: contains our package’s source code. - the
test
folder: because we are going to write tests! And we always write tests, right? - the
README.md
file: for a prose intro to the package. - the
LICENSE.md
file: specifying the license details will help developers understand what they can do with our code, and how. - the
.scrutinizer.yml
,.styleci.yml
and.travisci.yml
files: Scrutinizer, StyleCI and TravisCI are awesome (and free for open source projects) services. They analyze code quality, fix style issues and run tests respectively.
Before we proceed, a quick note on tests: don’t overtest, but also don’t undertest. It’s 2017, it shouldn’t be necessary to warn people about this, but I still see many packages without a single test to assert their quality (pun totally intended).
Into the Code: The Domain
When writing a package, a good approach is to abstract the domain logic we want to use to solve the problem, and then code the implementation. Laravel has a good service container which allows us to bind an interface to a concrete class and keep our code loosely coupled: let’s use it.
First stop: the GitHub repository of the package. In the src
folder there’s a Domain
folder: let’s open it. Feel free to explore the code. Can you see any Laravel-specific code? No, because our domain logic should remain separated from the real implementation.
LaravelFeature\Domain\Model\Feature
is a good example. It describes what a feature is, and what its functionalities are. Every feature has its own name, and its own status (enabled or disabled in the system).
<?php
namespace LaravelFeature\Domain\Model;
class Feature
{
private $name;
private $isEnabled;
public static function fromNameAndStatus($name, $isEnabled)
{
$feature = new self($name, (bool) $isEnabled);
return $feature;
}
private function __construct($name, $isEnabled)
{
$this->name = $name;
$this->isEnabled = $isEnabled;
}
public function getName()
{
return $this->name;
}
public function isEnabled()
{
return $this->isEnabled;
}
public function setNewName($newName)
{
$this->name = $newName;
}
public function enable()
{
$this->isEnabled = true;
}
public function disable()
{
$this->isEnabled = false;
}
}
We can also choose a new name, and decide to enable or disable it: that’s what the setNewName
, enable
and disable
methods are here for. Finally, the fromNameAndStatus
factory method lets us create our object in the best (and most expressive) way possible without using a constructor.
Let’s take a look at the FeatureManager
class. Every operation we do on features starts here (we will soon see how). It works with Feature
objects and an instance of a class that implements the LaravelFeature\Domain\Repository\FeatureRepositoryInterface
.
Let’s open it.
<?php
namespace LaravelFeature\Domain\Repository;
use LaravelFeature\Domain\Model\Feature;
use LaravelFeature\Featurable\FeaturableInterface;
interface FeatureRepositoryInterface
{
public function save(Feature $feature);
public function remove(Feature $feature);
public function findByName($featureName);
public function enableFor($featureName, FeaturableInterface $featurable);
public function disableFor($featureName, FeaturableInterface $featurable);
public function isEnabledFor($featureName, FeaturableInterface $featurable);
}
The concept is simple: if our FeatureManager
works with an interface instead of a concrete class, we can bind whatever we want to it and not worry about anything else. This is a good practice, especially if we’re working on a package: the best thing we can do is to give our developers the most possible flexibility.
That’s all for the domain logic.
Into the Code: The Implementation
The Repository
Now that we are done with the domain logic, it’s time to switch to the implementation. Let’s start from the concrete class we will “bind” to the interface we just saw.
The EloquentFeatureRepository is the class that will implement the FeatureRepositoryInterface. Laravel uses Eloquent by default, so providing a default way to use Eloquent as base is definitely a good idea.
We are not going to go into the details of the repository: it does some pretty basic things. Let’s see how to bind to a service provider instead. Service providers are the best place to register new bindings in our service container (and in our application). If you use Laravel often, you probably registered a service provider in the config/app.php
file for 99% of the packages installed. For my package, the FeatureServiceProvider is the chosen one.
The magic happens in the register()
method:
...
$config = $this->app->make('config');
$this->app->bind(FeatureRepositoryInterface::class, function () use ($config) {
return app()->make($config->get('features.repository'));
});
The FeatureRepositoryInterface
is bound to the EloquentFeatureRepository
class (it’s the default value of the features.repository
item in the config file).
Oh, about that…
The Config File
Another good practice while creating Laravel packages is to provide a config file, so the final developer can publish and use it to customize the package to better suit their business needs. To add a config file to the package, all we have to do is choose a place for it. Using a descriptive name is the best choice: here’s the one I added for the package.
In the service provider we previously made, the
$this->publishes([
__DIR__.'/../Config/features.php' => config_path('features.php'),
]);
call makes sure that when publishing vendor assets, we are also going to get a shiny features.php
file in the main project’s config
folder.
Also, another call to
$this->mergeConfigFrom(__DIR__.'/../Config/features.php', 'features');
is perfect to let the developer choose what to overwrite, and what to leave as it is. Less bootstrapping code to write, more happy developers!
Now let’s go back to the service provider. We can also see two calls to a couple of private methods: registerBladeDirective
and registerConsoleCommand
. Let’s start with the first, introducing…
The Blade Directive
A Blade directive to check if a feature is enabled or not can be a good idea. Its implementation is very simple: a single instruction in the registerBladeDirective
method.
private function registerBladeDirective()
{
Blade::directive('feature', function ($featureName) {
return "<?php if (app('LaravelFeature\\Domain\\FeatureManager')->isEnabled($featureName)): ?>";
});
Blade::directive('endfeature', function () {
return '<?php endif; ?>';
});
}
With these simple directive()
calls, we can now use @feature
and @endfeature
in our Blade templates. If the feature passed as parameter is enabled in the system, the code in the block will be shown. Otherwise, it will be hidden.
By the way, putting this code in the service provider is not always a good idea. In this specific case, we’re talking about a couple of calls only. Consider a separate class to do it if there’s more complex logic behind it, or maybe a separate provider if you want to let the developer decide which package features must be enabled.
The Console Command
Now, the final part: what about a command which scans all our project’s views to find @feature
directives and automatically adds those features to the system using the FeatureManager
? We are going to implement this using a console command: let’s see how to register a new one in a package.
Here’s what’s in the registerConsoleCommand
method.
private function registerConsoleCommand()
{
if ($this->app->runningInConsole()) {
$this->commands([
ScanViewsForFeaturesCommand::class
]);
}
}
What we are doing here is simple: when “this application is running in console”, add the ScanViewsForFeaturesCommand
command to the list of commands the developer can run.
Apart from this, there’s nothing really special to see here. A pretty linear process. However… something is still missing, right?
The Facade
Oh, yeah! The facade is still missing! Giving a facade to our developers means also giving them a nice tool to improve the package adoption.
Let’s build it, and place it in an appropriate place.
<?php
namespace LaravelFeature\Facade;
use LaravelFeature\Domain\FeatureManager;
use Illuminate\Support\Facades\Facade;
class Feature extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return FeatureManager::class;
}
}
We are binding the facade to the FeatureManager
class. This means that if the developer adds it to config/app.php
, they can call
Feature::enable();
instead of creating an instance of the FeatureManager
class and then calling enable()
on it. Quite an improvement (not every time, but we’re not going to cover the facade drama today)!
Advice
Let’s end this article with some generally applicable advice:
- use git tags to define versions for the package. After pushing to Github, it should be possible to see the new version already on Packagist.
- remember to double check the dependencies in the
composer.json
file. When working with some of Laravel’s components, you must be sure to include the rightilluminate/
package as a dependency. In my case, I used components fromilluminate/database
andilluminate/support
. - the Laravel ecosystem offers various packages to help with tests. Sure, PHPUnit already does tons of things, but packages like
mockery/mockery
andorchestra/testbench
can help big time. - write as much documentation as possible. If we want to see our package spread around the web, the first step is to explain all its possibilities. Writing examples is a good thing, explaining theory and concepts (when necessary) is a good thing. Doing it in a good English is even better!
Conclusion
This is the workflow I follow when developing Laravel packages – what about you? Have you ever had other experiences in package development that you want to share? Please leave a comment below – let’s make a good package building workflow together!