My process for writing Laravel packages

David Carr

Books Tools

Introduction

On my machine, I have a package folder. From here I make a folder for each package.

Laravel Packages

I then load these into my projects using `composer.json`

In the require section load the package by its vendor name followed by the package name then using @dev to load the main version 

"dcblogdev/laravel-module-generator": "@dev"

Next in order to load this, tell composer where to find the files locally using a repository path:

"repositories": [
    {
        "type": "path",
        "url": "../../packages/laravel-module-generator"
    }
]

In this case, I need composer to look two folders above the current Laravel project. 

Now run `composer update` to install the package into your project.

What’s good about this approach is you can do this with third-party packages. This is useful as you can run tests and add features that you plan to raise pull requests for.

Build a package

You can use package starter kits such as https://github.com/spatie/package-skeleton-laravel

I tend to build packages from scratch or copy one of my existing packages and remove what I don’t need.

In this post I'm imagining building a package called laravel-tags.

The folder structure will be:

Package Structure

license.md

For the license, I use The MIT licence which allows users to freely use, copy, modify, merge, publish, distribute, sublicense, and sell the software and its associated documentation.

this file contains:

# The license

The MIT License (MIT)

Copyright (c) 2023 dcblogdev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

composer.json

I'll start by adding a `composer.json` file that contains:

{
  "name": "dcblogdev/laravel-tags",
  "description": "A Laravel package for adding tags to your content",
  "license": "MIT",
  "authors": [
    {
      "name": "David Carr",
      "email": "dave@dcblog.dev",
      "homepage": "https://dcblog.dev"
    }
  ],
  "homepage": "https://github.com/dcblogdev/laravel-tags",
  "keywords": [
    "Laravel",
    "Tags"
  ],
  "require-dev": {
    "orchestra/testbench": "^8.0",
    "pestphp/pest": "^v2.24.2",
    "pestphp/pest-plugin-type-coverage": "^2.4",
    "laravel/pint": "^1.13",
    "mockery/mockery": "^1.6"
  },
  "autoload": {
    "psr-4": {
      "Dcblogdev\\Tags\\": "src/",
      "Dcblogdev\\Tags\\Tests\\": "tests"
    }
  },
  "autoload-dev": {
    "classmap": [
      "tests/TestCase.php"
    ]
  },
  "extra": {
    "laravel": {
      "providers": [
        "Dcblogdev\\Tags\\TagsServiceProvider"
      ],
      "aliases": {
        "Tags": "Dcblogdev\\Tags\\Facades\\Tags"
      }
    }
  },
  "config": {
    "allow-plugins": {
      "pestphp/pest-plugin": true
    }
  },
  "scripts": {
    "pest": "vendor/bin/pest --parallel",
    "pest-coverage": "vendor/bin/pest --coverage",
    "pest-type": "vendor/bin/pest --type-coverage",
    "pint": "vendor/bin/pint"
  }
}

Breaking this down, first the package needs a name in the convention of vendor name and package name `dcblogdev/laravel-tags` in my case the vendor name is `dcblogdev` and package name is `laravel-tags`

Add a require section if your package has dependencies 

In a require-dev section I import these third-party packages for development only

`testbench` package allows you to use Laravel conventions for testing.
`pest` is my testing framework of choice
`pint` to apply styling conventions to my codebase.

"orchestra/testbench": "^8.0",
"pestphp/pest": "^v2.24.2",
"pestphp/pest-plugin-type-coverage": "^2.4",
"laravel/pint": "^1.13",
"mockery/mockery": "^1.6"

Inside autoload to any folders that you want composer to autoload. I have a folder called `src` which will contain the classes and `tests` for all the tests 

One important aspect of this package name I'll often give the package a nickname to use so instead of using `laravel-tags` I'll use `tags`

I'll do this by using `Tags` in any classes namespace and creating an alias:

"extra": {
  "laravel": {
    "providers": [
      "Dcblogdev\\Tags\\TagsServiceProvider"
    ],
    "aliases": {
      "Tags": "Dcblogdev\\Tags\\Facades\\Tags"
    }
  }
}

Tests

Now I will create 2 folders:

1) src
2) tests

Inside tests I'll create these files:

TestCase.php

<?php

namespace Dcblogdev\Tags\Tests;

use Dcblogdev\Tags\TagsServiceProvider;
use Orchestra\Testbench\TestCase as Orchestra;

class TestCase extends Orchestra
{
    protected function getPackageProviders($app)
    {
        return [
            TagsServiceProvider::class,
        ];
    }

    protected function getEnvironmentSetUp($app)
    {
        $app['config']->set('database.default', 'mysql');
        $app['config']->set('database.connections.mysql', [
            'driver' => 'sqlite',
            'host' => '127.0.0.1',
            'database' => ':memory:',
            'prefix' => '',
        ]);
    }

    protected function defineDatabaseMigrations()
    {
        $this->loadLaravelMigrations();

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

The methods `getEnviromentSetup` and `defineDatabaseMigrations` are not needed by default. They are only required if you need to use a database and load migrations.

Inside `getPackageProviders` the main package service provider is loaded.

Now create a file called Pest.php

<?php

use Dcblogdev\Tags\Tests\TestCase;

uses(TestCase::class)->in(__DIR__);

This file calls a `uses()` function to load the tests case I'll use `__DIR__` inside the `in()` method to make Pest use the TestCase class in the root of this directory.

Inside the package root create a phpunit.xml file containing:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php"
         colors="true"
>
  <source>
    <include>
      <directory suffix=".php">src/</directory>
    </include>
  </source>
  <testsuites>
    <testsuite name="Test">
      <directory suffix="Test.php">./tests</directory>
    </testsuite>
  </testsuites>
  <php>
    <env name="APP_KEY" value="base64:2fl+Ktv64dkfl+Fuz4Qp/A75G2RTiWVA/ZoKZvp6fiiM10="/>
  </php>
</phpunit>

This sets the location for tests to read from, it's rare this file will need to be changed from this default.

I set an `APP_KEY` as standard for running tests, its value is not important.

Pint

To setup find create a folder in the package root called pint.json I use the Laravel preset:

{
    "preset": "laravel"
}

ReadMe

inside the project root create a file called readme.md to document the project, I typically use this format:

# Laravel Tags

Explain what the package does.

# Documentation and install instructions 
[https://dcblog.dev/docs/laravel-tags](https://dcblog.dev/docs/laravel-tags)

## Change log

Please see the [changelog][3] for more information on what has changed recently.

## Contributing

Contributions are welcome and will be fully credited.

Contributions are accepted via Pull Requests on [Github][4].

## Pull Requests

- **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date.

- **Consider our release cycle** - We try to follow [SemVer v2.0.0][5]. Randomly breaking public APIs is not an option.

- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.

## Security

If you discover any security related issues, please email dave@dcblog.dev email instead of using the issue tracker.

## License

license. Please see the [license file][6] for more information.

[3]:    changelog.md
[4]:    https://github.com/dcblogdev/laravel-tags
[5]:    http://semver.org/
[6]:    license.md

ServiceProvider

Inside src create the service provider in my case TagsServiceProvider.php

<?php

namespace Dcblogdev\Tags;

use Illuminate\Support\ServiceProvider;

class TagsServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        $this->configurePublishing();
    }

    public function configurePublishing(): void
    {
        if (! $this->app->runningInConsole()) {
            return;
        }

        $this->publishes([
            __DIR__.'/../config/tags.php' => config_path('tags.php'),
        ], 'config');
    }

    public function register(): void
    {
        $this->mergeConfigFrom(__DIR__.'/../config/tags.php', 'tags');

        // Register the service the package provides.
        $this->app->singleton('tags', function () {
            return new Tags;
        });
    }

    public function provides(): array
    {
        return ['tags'];
    }
}

Inside the `boot` function set called to other methods, to begin with I only have one called `configurePublishing` This will publish any files defined in this page and will only run when the class is ran from a CLI

The `register` method allows a published config file to be merged in with a local `./config/tags.php` config file

And setup the main class to be instantiated.

Facades

If you want to make use of facade ie have static called to a class that would normally be instantiated.

Inside src create a folder called facades and your class such as Tags.php

<?php

namespace Dcblogdev\Tags\Facades;

use Illuminate\Support\Facades\Facade;

class Tags extends Facade
{
    protected static function getFacadeAccessor(): string
    {
        return 'tags';
    }
}

 

Resources 

For more details read https://www.laravelpackage.com

Read articles directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Copyright © 2006 - 2024 DC Blog - All rights reserved.