Byte Ebi's Logo

Byte Ebi 🍀

A Bit everyday A Byte every week

Develop Your Own Laravel Package

Guide on developing and testing Laravel packages locally and installing them using composer in a local project.

Ray

The Laravel ecosystem offers numerous convenient packages for various purposes.
However, what if you can’t find a suitable package or need to create a company-specific one?
This article demonstrates how to develop a package locally and install it in a Laravel project using composer, ensuring smooth development and testing.

Some articles suggest creating a packages folder within an existing Laravel project for package development.
However, this approach has drawbacks. Imagine deleting your project one dayβ€”your package project would be deleted too.
Additionally, this method doesn’t facilitate testing the installation via composer. This article presents an alternative approach.

1. Initialize Package Project

First, create a folder for your package; for example, we’ll use larapeko as a sample.

mkdir larapeko

Navigate to the newly created larapeko folder and run the following command to initialize the package.

cd larapeko
composer init

You’ll be prompted with a series of questions to configure basic package information.

larapeko composer init

Afterward, you’ll find the composer.json file in the folder.

{
    "name": "ray247k/larapeko",
    "description": "A package for demo peko",
    "license": "MIT",
    "authors": [
        {
            "name": "Ray",
            "email": "[email protected]"
        }
    ],
    "minimum-stability": "dev",
    "require": {}
}

2. Write Package Code

Create a src folder in the larapeko directory to host the package code.
Inside, create a file named LaraPeko.php and set the appropriate namespace.
For demonstration purposes, let’s start with a simple function.

<?php
# LaraPeko.php

namespace Ray247k\LaraPeko;

class LaraPeko
{
    public function sayPeko()
    {
        echo "peko peko\n";
    }
}

3. ServiceProvider

Create a ServiceProvider file in the src directory, named LaraPekoServiceProvider.php.
In the register method, use a singleton to register a class named LaraPeko and return a new instance of it.

<?php
# LaraPekoServiceProvider.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\ServiceProvider;

class LaraPekoServiceProvider extends ServiceProvider
{
    public function boot()
    {

    }

    // Register the package
    public function register()
    {
        $this->app->singleton('LaraPeko', function ($app) {
            return new LaraPeko();
        });
    }
}

4. Facade

Facades provide a static interface to call registered classes directly.
Create a Facade file in the src directory, named LaraPekoFacade.php, and use the LaraPeko class registered in the ServiceProvider as the Laravel Facade object.

<?php
# LaraPekoFacade.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\Facades\Facade;

class LaraPekoFacade extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'LaraPeko';
    }
}

5. Default Config and Usage

Sometimes, users may want to customize parameters through a configuration file.
Create a config folder in the package directory and add a configuration file, lara_peko.php.

<?php
# lara_peko.php

return [
    'best_girl' => 'Yagoo',
];

Edit the LaraPekoServiceProvider.php file to include publishing the configuration file.

<?php
# LaraPekoServiceProvider.php

namespace Ray247k\LaraPeko;

use Illuminate\Support\ServiceProvider;

class LaraPekoServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $source = realpath($raw = __DIR__.'/../config/lara_peko.php') ?: $raw;
        $this->publishes([
            $source => config_path('lara_peko.php'),
        ]);
    }

    // Register the package
    public function register()
    {
        $configPath = __DIR__ . '/../config/lara_peko.php';
        $this->mergeConfigFrom($configPath, 'lara_peko');

        $this->app->singleton('LaraPeko', function ($app) {
            return new LaraPeko();
        });
    }
}

After running php artisan vendor:publish, choose the corresponding number for the package when prompted.
This will generate a configuration template file in the project’s config directory.

If you are sure which package’s config file you want to create, you can directly specify the provider tag.

php artisan vendor:publish --provider="Ray247k\LaraPeko\LaraPekoServiceProvider"

In the register method, the default and new configuration files are merged.
If users modify the project’s configuration file, it will override the default configuration, allowing for customization.

Clear Configuration Cache

When the configuration changes, use the following commands to clear the project’s configuration cache.

php artisan config:clear   # Clear the configuration cache
php artisan cache:clear    # Clear the general cache

Using Config Parameters

To use the configuration in your code, employ the config() method.
In the LaraPeko.php file, create a getBestGirl method to retrieve the best_girl parameter from the configuration.

<?php
# LaraPeko.php

namespace Ray247k\LaraPeko;

class LaraPeko
{
    public function sayPeko()
    {
        echo "peko peko\n";
    }

    public function getBestGirl()
    {
        echo config('lara_peko.best_girl');
    }
}

6. Package Auto-discovery

Introduced in Laravel 5.5, this feature allows users to reduce the steps required during package installation by configuring the package settings. The goal is to eliminate manual editing of the config/app.php file’s providers and aliases arrays after running composer install.

Keywords: Package Auto-discovery, Laravel Extension Package Auto-discovery.

Procedure: Add the following content to the autoload and extra sections in the package’s composer.json.
Due to length constraints, only the added parts are shown:

{
  "autoload": {
    "psr-4": {
      "Ray\\LaraPeko\\": "src"
    }
  },
  "extra": {
    "laravel": {
      "providers": [
        "Ray\\LaraPeko\\LaraPekoServiceProvider"
      ],
      "aliases": {
        "LaraPeko":  "Ray\\LaraPeko\\LaraPekoFacade"
      }
    }
  }
}

Through the laravel definition in the extra section, a data entry is added to the providers and aliases arrays for registration.
The autoload uses the PSR-4 rule to point the specified namespace to the folder where our package code is located (src).

7. Package Dependencies

If your package depends on another package, such as the commonly used Guzzle, include the required item in the package’s composer.json:

{
    "name": "ray247k/larapeko",
    "description": "A package for demo peko",
    "license": "MIT",
    "authors": [
        {
            "name": "Ray",
            "email": "[email protected]"
        }
    ],
    "minimum-stability": "dev",
    "require": {
        "php": ">=7.3",
        "laravel/framework": "5.5.*||^6.0|^7.0|^8.0",
        "guzzlehttp/guzzle": "^6.3"
    }
}

In addition to requiring Guzzle, we have specified the required PHP version and Laravel framework version.

8. Package Testing

Setting up a Test Project

After the above process, our package is almost configured. Next, we’ll enter the testing phase.
Since we intend to use it in a Laravel project, let’s create a new Laravel project for testing, named “lara-ahoy”:

laravel new lara-ahoy

You can check the installed Laravel version with the following command:

cd <project directory>
php artisan -V
# Laravel Framework 6.20.30

Including the Package in the Project

Add Package Entry

To use the package in the project, use the following command to add the package’s version storage location to the composer.json:

composer config repositories.ray247k path ../larapeko

This assumes that the package (larapeko) and the test project (lara-ahoy) are in the same directory.

After running the command, open the composer.json file in the lara-ahoy project, and you’ll see a new section:

{
    "repositories": {
        "ray247k": {
            "type": "path", 
            "url": "../larapeko"
        }
    }   
}

You can manually add this section if you prefer not to use the command.

If the package is in a remote repository, change the type to vcs and add the URL:

composer config repositories.ray247k vcs https://github.com/ray247k/larapeko

Or manually adjust the repositories setting in composer.json:

{
    "repositories": {
        "ray247k": {
            "type": "vcs", 
            "url": "https://github.com/ray247k/larapeko"
        }
    }   
}

The remote URL can use either https or ssh. If the repository is private, use ssh
(e.g., [email protected]:ray247k/larapeko.git).

Add Dependency

You can use the following command:

composer require ray247k/larapeko @dev

Or manually add the following content to the require section in composer.json:

{
    "require": {
        "ray247k/larapeko": "@dev"
    }
}

If an error occurs during the require command due to an issue with a dependency’s version, you can use the --ignore-platform-reqs parameter to ignore the platform:

composer require ray247k/larapeko --ignore-platform-reqs

After adding the local dependency, it will automatically fetch the latest code each time it is used.
If it doesn’t automatically fetch or if you prefer to run composer require every time to update, adjust the symlink setting in the test project’s composer.json under the repositories section:

{
    "repositories": [
        {
            "type": "path",
            "url": "../../packages/my-package",
            "options": {
                "symlink": false
            }
        }
    ]
}

Install the Package

Install the package using:

composer install

In the output, you should see that the package has been discovered and installed:

Discovered Package: ray247k/larapeko
Package manifest generated successfully.

If a version error occurs similar to during the require command, use the --ignore-platform-reqs parameter:

composer install --ignore-platform-reqs

Open the composer.lock file in the lara-ahoy project to verify that the package has been successfully installed.

If you have previously run the project, you must clear the existing autoload cache:

composer dump-autoload

Testing the Package

In the previous steps, we installed our larapeko package into the test project lara-ahoy.
Now, we’ll test whether the package has been correctly integrated using two methods: file testing and unit testing.

Method 1: File Testing

Create a file named test-autoload.php directly in the Laravel project’s root directory.
In this file, load the autoload and call the package method:

<?php
# test-autoload.php

use Ray247k\LaraPeko\LaraPeko;

require_once './vendor/autoload.php';
LaraPeko::sayPeko();

Run the test file in the lara-ahoy project directory:

php test-autoload.php

If the autoload is successful, you should see the result of the sayPeko() method, even if a PHP Deprecated message appears:

file test

Although a PHP Deprecated message is displayed, the default string content is still successfully printed.
This indicates that the Facade aliases registered in the ServiceProvider were called successfully!

To suppress the PHP Deprecated message, add error_reporting(0); to the test-autoload.php file:

<?php

error_reporting(0);

use Ray247k\LaraP

eko\LaraPeko;

require_once './vendor/autoload.php';

LaraPeko::sayPeko();

Now, you won’t see the PHP Deprecated message:

peko test

Method 2: Unit Testing

Next, let’s use PHPUnit to perform unit tests. Run the following command in the project directory:

php vendor/bin/phpunit

If you encounter an error related to an invalid value for PHP_VERSION, consider using Homebrew to install PHP or running the tests in a Docker environment.

If php vendor/bin/phpunit works, proceed to build the unit test file.

Add a testing method getAhoy() to LaraPeko.php:

public function getAhoy()
{
    return "ahoy";
}

Create a tests folder inside the package project and add a test file named LaraPekoTest.php:

<?php

namespace Ray247k\LaraPeko;

use PHPUnit\Framework\TestCase;

class LaraPekoTest extends TestCase
{
    /**
     * @test
     *
     * @return void
     */
    public function testClassInstance()
    {
        $this->assertInstanceOf(LaraPekoTest::class, new LaraPekoTest);
    }

    public function testGetAhoy()
    {
        $larapeko = new LaraPeko();
        $this->assertEquals('ahoy', $larapeko->getAhoy());
    }
}

This test checks whether the class instance is correctly created and whether the getAhoy() method returns “ahoy”.
Run the test using the following command:

php vendor/bin/phpunit vendor/ray247k/larapeko/tests

You should see the PHPUnit results:

phpunit result

Note: Suppose there is a private function testMethod() in the LaraPeko package:

private function testMethod()
{
    return 'Haha';
}

To test a private function, you can use the bindTo() method within a closure.
After creating an object, manually inject it into the closure object to replace $this in the closure.
This way, it’s like directly calling the testMethod() method using the LaraPeko object:

/**
 * @test
 *
 * @return void
 */
public function testMethodTest()
{
    $contentSegment = new LaraPeko();
    $closure = function () {
        return $this->testMethod();
    };

    $closure_bind = $closure->bindTo($contentSegment, $contentSegment);

    $this->assertEquals('Haha', $closure_bind());
}

This allows testing private methods effectively!


The above is a comprehensive guide for developing your own Laravel package.
If you have any further questions or topics to discuss, feel free to let me know. I’m happy to assist!

Recent Posts

Categories

Tags