Get my latest Figma template "Calais"
Create your own Laravel package

Create your own Laravel package

So in this article I'm going to show you step by step how you would create a package for Laravel. When I started creating Laravel packages, it was quite difficult to get some proper information, so I ended up checking existing packages and learn how they were created.

For me packages by Spatie were a great inspiration. Their packages are well structured and their code is well written and documented, so it was a really starting point.

What is a Laravel package?

Laravel as a framework comes with a lot of features that make creating web applications really easy. Authentication? Laravel got you covered! Form Validation? Yeah, we've got that! File System? Of course!

Though we have a lot of features, there are still some you might miss. Though, extending the Laravel core isn't that difficult, it's done using packages.

Laravel itself provides some packages like Passport, Scout or Socialite, but there are - like Spatie - awesome companies that commit really awesome open source Laravel packages to community.

In this article I want to show you, how you can create a package on your own, that you may publish as well.

Prerequisites

Following steps are taken with having in mind to ship this package for Laravel 5.5 (current version as of Feb 04th, 2018), but we are not limited to this version only. By slightly changing some dependecies you can make you package work for other versions from 5.3 - 5.6 as well.

You can also use these steps to make your package work for Laravel 5.0 - 5.2, but I won't guarantee anything. Remember that the more versions you support the more you have to make sure that your package is backwards compatible. Also you won't be able to use 5.5 features in a package that also has to work with Laravel 5.2.

For developing a Laravel package you just need composer. And as you are working with Laravel already, I'm pretty sure you are using composer already. If not just make sure you have it installed. You'll get the instructions on the getting started page on their website.

You should also have some little experience with Laravel as you are going to extend it. While developing you won't have a Laravel project installed to check your packages, but start from scratch.

Step 1: The directory structure

There is no rule on how to design you package. Apart from little exceptions you can structure your package as you want to. Laravel as a framework does not expect anything from you at this point.

Nevertheless it makes sense to have a certain structure to ensure that your result will be a clean and maintainable repository.

The following structure is how I basically create packages:


├── database/
│   ├── .gitkeep
├── config/
│   ├── package-name.php
├── src/
│   ├── PackageNameServiceProvider.php
├── tests/
│   ├── TestCase.php
├── .gitignore
├── CHANGELOG.md
├── composer.json
├── LICENSE
├── phpunit.xml
├── README.md

That's the backbone of every package I work on. So let's have a detailed look at it:

The database/ and the config/ directories are the same as you know from common Laravel projects. In database I have all the migrations, seeds, and factories. In config/ I store my config file of this package. (Never had the situation that I had more than a single config file so far.)

The src/ directory is the location for all the logic of the package. Starting with the service provider src/ can contain anything related to the package.

As you may have guessed tests contains the tests.

This is not a mandatory structure, so you don't have to structure your packages this way. If you don't need any configuration, you don't need config/ at all, if your package doesn't touch the database, you don't need database/. You may even move these two directories into src/, it's all up to you.

But if you consider to publish and open source your package, you should care about a consistent structure, no matter how you define it.

Step 2: composer init

Now as we have our structure, we can start creating the backbone of your package.

Create a new directory with the name of your package, enter it and initialze a composer file:

$ mkdir awesome && cd awesome && composer init

This will start an interactive CLI programm. Just run through it until you reach the question for interactivly defining the dependencies. Here you can already define at least two:

  1. You need illuminate/support for the service provider
  2. With defining php you can define the minimum PHP version that your package will support

If you want to test you application, you may also add the following packages as dev-dependencies:

  1. phpunit/phpunit
  2. orchestra/testbench

So what version should you use? At the beginning of this article, I mentioned that I'm gonna create a Laravel package for 5.5, so I'm gonna choose this, but you can define what ever you want, for example 5.3|5.4|5.5 if you want to support multiple Laravel versions. Get the detailed information on versioning and constraints on the composer website.

At the end of this program you should have a composer.json file. This is the output of my file:

{
  "name": "aheenam/awesome",
  "description": "An awesome Laravel Package",
  "license": "MIT",
  "authors": [],
  "minimum-stability": "stable",
  "require": {
    "php": "^7.0",
    "illuminate/support": "~5.5.0"
  },
  "require-dev": {
    "phpunit/phpunit": "5.*|^6.3",
    "orchestra/testbench": "~3.4.0|~3.5.0"
  }
}

Now run composer install to install the dependecies you defined.

Autloading

When someone installs your package for his Laravel project, the autoloader of his project will look up in your composer.json if there is anything to autoload. This is missing right now, so we have to add this:

"autoload": {
    "psr-4": {
        "Aheenam\\Awesome\\": "src"
    }
},
"autoload-dev": {
    "psr-4": {
        "Aheenam\\Awesome\\Test\\": "tests"
    }
},

So in my case, I just let Aheenam\Awesome map to src/ and Aheenam\Awesome\Test to tests/. Based on you structure you might have to add some more.

Package Discovery

In earlier versions of Laravel, you manually had to add the service provider of the package you installed to the projects config/app.php.

As of Laravel 5.5 there is a feature called "Package Discovery" that makes this step redundant.

But as Laravel does not expect you to structure your package in a specific way, you need another way to expose your packages service provider to the Laravel instance. That is also done in composer.json

"extra": {
    "laravel": {
        "providers": [
            "Aheenam\\Awesome\\AwesomeServiceProvider"
        ]
    }
},

This little entry makes the Laravel project to discover your packages service provider.

Step 3: The service provider

We have already linked the service provider in the composer.json file, but haven't created it yet. So let's do this:

Just create a file in src/. It's not uncommon to name the main service provider the same as the packages name, but you can name it as you want. Just make sure it matches to what you named it in you composer.json

$ touch src/AwesomeServiceProvider.php

The base structure of a service provider looks like following:

<?php

namespace Aheenam\Awesome;

use Illuminate\Support\ServiceProvider;

class AwesomeServiceProvider extends ServiceProvider
{
    /**
     * Indicates if loading of the provider is deferred.
     *
     * @var bool
     */
    protected $defer = false;

    /**
     * Bootstrap the application events.
     *
     * @return void
     */
    public function boot()
    {
    }


    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
    }
}

The service provider is basically the root of you package. Here you define which config files are loaded, where the view file can be found and which assets can be published. Get more information on service providers in the Laravel documentations

For now we leave it as it is. While developing the package, we will add some stuff to it.

Step 4: Testing

The last step of configuration before we start coding belongs to testing. This step is not mandatory, but I highly recommend to write tests for you packages (as I recommend tests for any type of writing software).

I use PHPUnit for testing purposes. Configuring PHPUnit is done via an phpunit.xml file in the root of our package.

There are a lot of config options, which you may find out in the docs of PHPUnit, but most of my packages just have following simple configuration, which may be enough for your needs as well:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         verbose="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false"
         syntaxCheck="false">
    <testsuites>
        <testsuite name="Aheenam Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist>
            <directory>src/</directory>
        </whitelist>
    </filter>
</phpunit>

Testbench

When creating simple PHP packages, we would use the TestCase provided by PHPUnit to write tests, but in our case we want our code to work in a Laravel environment. But we don't have one in our project.

To solve this issue, we use TestBench. It makes sure that we can create a package using all the benefits of Laravel.

So instead of using the TestCase by PHPUnit, we are now going to make use of the TestCase that Testbench provides us.

<?php

namespace Aheenam\Awesome\Test;

use Aheenam\Awesome\AwesomeServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;

abstract class TestCase extends Orchestra
{

    /**
     * Setup the test environment.
     */
    public function setUp()
    {
        parent::setUp();
    }

    /**
     * add the package provider
     *
     * @param $app
     * @return array
     */
    protected function getPackageProviders($app)
    {
        return [AwesomeServiceProvider::class];
    }

    /**
     * Define environment setup.
     *
     * @param  \Illuminate\Foundation\Application  $app
     * @return void
     */
    protected function getEnvironmentSetUp($app)
    {
        // Setup default database to use sqlite :memory:
        $app['config']->set('database.default', 'testing');
        $app['config']->set('database.connections.testing', [
            'driver'   => 'sqlite',
            'database' => ':memory:',
            'prefix'   => '',
        ]);
    }
}

Testbench gives us a basic Laravel setup. We now have to update this setup adding our own package. This means that we at least add the packages service provider to the environment. In our example above, I also change the default database configuration to testing which I setup as an in-memory sqlite database.

There are some more options, so please make sure to cHeckout the GitHub page of Testbench.

Step 5: Controllers & Routes

So now starts the interesting part, but also the part I do not need to explain that much. You can start coding now.

In my simple "Hello World" package, I just want to add an route that responds with greeting the name given in this route.

So for example GET /hello/john would create a response view that says "Hello John".

To realize this, we need a route. So let's create a route file. The location of the route file is up to you, I like to follow the concept of Laravel having a routes/ directory and inside of it, having a web.php or api.php or whatever I need:

$ touch src/routes/web.php

In this case I need a web.php with following content:

<?php

Route::get('/test/{name}')
    ->uses('Aheenam\Awesome\Controllers\TestController@index');

Make sure that you add illuminate/routing as a dependecy to your composer.json.

So we now have defined our route and set it up to use the index() function of a TestController, so let's create this controller as well:

$ touch src/Controllers/TestController.php

The TestController looks like a controller of a Laravel project, just with the exception that we extend the base controller of illuminate/routing instead of the one in the app/Controllers directory of a Laravel project.

<?php

namespace Aheenam\Test\Controllers;

use Illuminate\Routing\Controller;

class TestController extends Controller
{

    /**
     * @param $name
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
     */
    public function index($name)
    {
        return view('test::index', [
            'name' => $name
        ]);
    }

}

So our index() method returns a view passing the name as a variable to this view.

So now we have to add this view as well. For working with views, we also need to add illuminate/view as a dependecy. So add this to your composer.json as well.

Register routes

Currently we have defined a route, added the controller and its method, but the route will never be triggered as we haven't registered it yet. So right now, a project using out package would just look up for the routes in the projects routes/ directory, but will never load the web.php of our package.

To make sure that the routes are loaded correctly, we have to modify the boot() function of our service provider as following:

/**
 * Bootstrap the application services.
 *
 * @return void
 */
public function boot()
{
    $this->loadRoutesFrom(__DIR__.'/routes/web.php');
}

Step 6: The view

We have already added illuminate/view as a dependency, and in our controller we are loading test::index as the view.

Note the test:: prefix? This says that the view method should not be looked up in the normal views directory, but in the directory that was tagged with this prefix. In our case we want it to look for the view file in our package, so let's create the file:

$ touch resources/views/index.blade.php

We still have to define that the views having test:: as a prefix should be loaded from our newly created directory. As we did for the routes, we can do this in our service provider. This time we call a method in the register() function:

/**
 * Register the application services.
 *
 * @return void
 */
public function register()
{
    $this->loadViewsFrom(__DIR__.'/../resources/views', 'test');
}

The first parameter of loadViewsFrom() is the path of our views directory, the second one is the prefix for this location.


Following this way you can now start developing your package. Like we did for routes and views, you can also load config files, translations and more. Just check out the Laravel docs for package development for all the possibilities.

Publish your Laravel package

That were six steps to create your first Laravel package. Now you may think, that your package is interesting for other developers as well. You should then make the package available for others via packagist.

But before that, you should prepare your package and make it publish-ready:

  1. README: You know what your package does, but others don't, so please provide a useful documentation of the package. Information on how to install and configure the package and information of the basic usage of the package should be added to a README.md
  2. Contribution Guide: If and how other developers can contribute to your package should be defined in a CONTRIBUTION.md
  3. LICENSE: Under what license do you want to publish your package? Choose one and add the appropriate license information to a LICENSE file. A good starting point for choosing a license is the Choose an open source license website!

Once you have made your package ready for publication, go to the packagist website, sign in with your GitHub account and submit your repository. That's really easy.

Laravel Package Generator

As you may have noticed, all the steps except for #5, are nearly always the same. So to make life easier, I created a little generator to take this scaffold work out of your hands.

Install the project globally with

$ composer global require aheenam/laravel-package-cli

and you are now able to create the base construct of a new package using

$ laravel-package generate vendor/package-name

Check out the GitHub page of Laravel Package CLI for further information. In case of feature requests or just for help, feel free to open an issue and contact me this way!