Laravel Package Development Basics

Writing packages is not easy, especially if we aim to do a good job. In this post, we want to make clear the basic concept of Laravel package development. We will cover some frequently asked questions, documented and undocumented stuff and some of our own experiences.

Why Should I Write Packages?

First of all, we must understand why packages are important. Developing packages has many good points, but for now, we will cover two of them – that we think the most important.

Writing a package means, you write code that can be reused and integrated by others. To make this happen you need to plan, develop, test, fix, plan again, develop again and so on. It opens your eyes up, forces you to practice a lot and helps you to write cleaner an better code.

On the other hand, it’s a good thing to give something to the Open Source world. Just imagine how many hours does Laravel save you when you start a new project. Maybe you have something as well, that could save hours of work for others, or maybe others could learn from. We think this is a nice way to “pay back”.

Preparing the Package Development

So first, let’s talk about the directory structure. It’s strongly suggested to use the naming conventions that Laravel uses. Also, in this case – unline a usual Laravel app – we put the Controllers, Models and so on in the src directory and not in the app.

| resources/
| tests/
| config/
| database/
| src/
| .gitignore
| CHANGELOG.md
| README.md
| phpunit.xml
| composer.json
| LICENSE

Like a Laravel app, in the resources folder, we store the views and the assets (CSS, JS) that the package uses. We use the test folder to keep our package Unit and Feature tests in. Also, there is the config folder where we put the configuration file. We keep the migrations in the database directory. In the src, we keep everything that we use under the package’s namespace: The service provider, models, controllers, middlewares, jobs, commands and so on. So in one word, we keep the same structure as in a Laravel app, but instead the app directory for the “core” files, we use the src.

Now let’s move on the composer.json file. In this file, we define the basic parameters of the package. We discover the whole schema here, but here is an example of a basic JSON file:

{
    "name": "thepinecode/package-name",
    "description": "This is the description of the package.",
    "type": "project",
    "license": "MIT",
    "authors": [{...}, {...}],
    "autoload": {
        "psr-4": {
            "Pine\\Package\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "Pine\\Package\\Tests\\": "tests/"
        }
    },
    "require": {...},
    "require-dev": {...},
    "extra": {
        "laravel": {
            "providers": [
                "Pine\\Package\\PackageServiceProvider"
            ]
        },
        "branch-alias": {
            "dev-master": "1.0-dev"
        }
    }
}

This is a very basic setup for the package’s composer.json file. Later we will cover some other possibilities that we may need during development.

As you see in the extra section, we provided a laravel object and a providers array inside of it. We can provide our service provider here and we don’t need to register it in the app.php config file, since it will be discovered automatically.

Setting Up the Development Environment

The best way to develop a package to pull it into a Laravel application like it would be an existing package. We can do it via composer (that’s why it was important to set up the package’s composer first). So what we can do to define repositories in our application’s composer.json and pull the pre-defined local package as a dependency.

...
"require": {
    "php": "^7.1.3",
    "fideloper/proxy": "^4.0",
    "laravel/framework": "5.7.*",
    "laravel/tinker": "^1.0",
    "thepinecode/package-name": "~1.0-dev"
},
...
"repositories": {
    "package-name": {
        "type": "path",
        "url": "packages/thepinecode/package-name",
        "options": {
            "symlink": true
        }
    }
},
...
We create a packages folder to the Laravel app’s root, and inside the structure we need. In this case thepinecode/package-name. We will store all the package files in the package-name directory.

We can note two things here: we define a repositories object and inside of it the local package and its details. Composer reads the local path, then it will be able to pull it as it would be a “normal” package. Also, at the require section, we add the package name and the version we defined at the package’s composer.json, at the extra section.

If we did everything well, we can run the composer install command to pull in all the required packages including our local package. From here, we can develop our package locally and see how the integration works with a Laravel application.

Package Configuration

Mostly, we may provide a configuration for our package. Following Laravel’s structure, we can put the config file in the src/config directory. We should give it a name that represents the package’s name. It makes reference the config easier application-wide.

To merge to config automatically to the application’s config, in the package service provider’s register() method we need to add the following code:

$this->mergeConfigFrom(__DIR__.'/../config/package-name.php', 'package-name');

From this point, we can reference this config file application-wide: config(‘package-name.something’). Now the question is, what if the user needs to override some option? We need to make the config available for the users. Also, we have the same issue with blade templates for example. Fortunately, we can easily do it, by allowing the users to publish – or copy if you wish – the overridable files from the package to the application.

In the service provider’s boot() method, we can use the publish() method to define the files that the user may override:

$this->publishes([
    // Config
    __DIR__.'/../config/package-name.php' => config_path('package-name.php'),
    
    // Views
    __DIR__.'/../resources/views/view-1.blade.php' => resource_path('views/vendor/package/view-1.blade.php'),
    __DIR__.'/../resources/views/view-2.blade.php' => resource_path('views/vendor/package/view-2.blade.php'),
    
    // Translations
    __DIR__.'/../resources/lang' => resource_path('lang/vendor/package-name'),
    
    // Assets
    __DIR__.'/../resources/js' => public_path('vendor/package-name/js'),
    __DIR__.'/../resources/css' => public_path('vendor/package-name/css'),
], 'package-name');

By running the php artisan vendor:publish ––tag=package-name command the given files will be copied to the given paths in the application. From now they are editable, and Laravel will use the overridden version of the published file.

Package Views

As we mentioned just right above, we can bring views with the package. It can be insanely useful in some cases. We already talked about how to publish the views with the config. Now let’s take a look at how to provide view files without publishing them. In the service provider’s boot() method we can add the following code:

$this->loadViewsFrom(__DIR__.'/../resources/views', 'package-name');

From this point, we can use the blade files easily: @include (‘package-name::view-1’). So we have the package-name prefix before the file and that’s it.

Package Routes

We have the possibility to define routes from the package. Imagine if you are building a package where you need to handle an incoming webhook. You need to provide the endpoint and the logic to handle behind the scenes. We can create controllers and middlewares and keep them in the package. It means if we define a route, we easily can reference to the package controller or use the middleware from the package.

Again, it’s strongly suggested to keep the Laravel directory structure in your package as well. It means you may put your controllers in the src/Http/Controllers directory and the middlewares in the src/Http/Middleware directory.

To add the routes to the application, we need to load the routes file from the package in the service provider’s boot() method:

$this->loadRoutesFrom(__DIR__.'/../routes/web.php');

We can define and use the routes in the same way as a normal application. No tricks here.

Package Migrations

Often we bring our own models with the package, that means we bring their own migration as well. Loading the migrations to the application is as easy as loading the routes. We need to add one line to the boot() method again and it’s ready to go.

$this->loadMigrationsFrom(__DIR__.'/../database/migrations');

When you run the php artisan migrate command, Laravel will automatically migrate the package’s migration as well without any extra effort.

Package Translations

Localization can be a frequent need. Like view files, you can publish and load them easily from the boot() method. We already covered the publish part but let’s take a look at how to load the translations:

$this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'package-name');

To use them, we need to prefix the translation key as we did with the views: @lang (‘package-name::messages.failed’).

Package Commands

To register any command in our package we have a dedicated method that we can use – surprisingly – in the boot() method.

$this->commands([
    Commands/CommandOne::class,
    Commands/CommandTwo::class,
]);

From this point, you can use the commands from the artisan CLI with the signature you defined for the commands.

Testing the Package

Testing is vital every time. Especially when you write something that others may use, you need to make sure – as much as you can – that your code does not break anything in others’ application.

Fortunately, we can easily pull in a composer package called orchestral/testbench. To add it to the package’s development dependencies we can run the composer require orchestral/testbench -–dev command.

It has nice documentation and with a little setup (especially if you store your config in the phpunit.xml file), you are ready to go and write your tests like it would be a normal Laravel app.

Try to cover your package by writing both feature and unit tests when the package’s functionality requires it. Also, you may follow the test coverage and try to improve your tests as much as you can.

Summary

We walked through only the basics of package development. There is a wide range of possibilities and really, you can implement anything you want. If you want to extend Response, Request or Collection with macros, you can. Maybe you need to schedule jobs or commands from your package? You can. We could continue the list all day.

Maybe you find a solution to a problem? Possibly others have similar problems. Why not share your solution? We think it’s a nice way to contribute to the Open Source world and besides that, you learn and practice a lot while helping others.

You can read the official documentation of package development here: https://laravel.com/docs/master/packages. We strongly suggest to read it, because you can read some more information that is not covered here.

Special thanks for the following recource(s): Icon made by Freepik from www.flaticon.com