How to Generate a Package in Laravel

How to Generate a Package in Laravel and Why It's Important

user

Manish Kumar

22 Jul 2024

12 min read

Laravel

Introduction

As a developer working on multiple projects, I noticed that some functionalities are often the same. To save time and ensure consistency, I built several packages for personal use. These include:

  1. CRUD Generator with Command Line: Automates CRUD operations via the command line.
  2. Enhanced CRUD Generator with UI: Adds a user interface for generating packages with options like relationships, datatables (normal or Livewire), and frontend frameworks (Bootstrap or Tailwind).
  3. Domain-Driven Design Generator: Helps structure existing projects based on domain-driven design principles.
  4. Test Case Generator: Automatically generates basic test cases for CRUD operations.

All these projects are currently private. In this blog, we'll focus on creating a simple package to understand the process better.

Why Create Packages?

Creating packages in Laravel can greatly enhance productivity by:

  • Reusability: Common functionalities can be reused across multiple projects.
  • Consistency: Ensures the same standards and practices are followed.
  • Modularity: Helps keep the codebase clean and organized.
  • Ease of Maintenance: Bugs and updates can be managed in a single place.

Creating a Laravel Package

Here’s a step-by-step guide to creating a Laravel package.

Step 1: Setup

Create a new folder named package-generator. Inside this folder, create a composer.json file with the following content:

{
    "name": "thecodersprint/package-generator",
    "description": "A package to generate Laravel packages",
    "autoload": {
        "psr-4": {
            "Thecodersprint\\\\PackageGenerator\\\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "Thecodersprint\\\\PackageGenerator\\\\PackageGeneratorServiceProvider"
            ]
        }
    },
    "require": {}
}

Step 2: Directory Structure

We will create the following directory structure inside your package:

package-generator/
├── config/
│   └── config.php
├── src/
│   ├── Console/
│   │   └── Commands/
│   ├── Http/
│   │   ├── Controllers/
│   ├── Resources/
│   │   ├── views/
│   ├── Routes/
│   │   └── web.php
│   └── PackageGeneratorServiceProvider.php
└── composer.json

Step 3: Service Provider

Create a PackageServiceProvider.php in the src folder:

<?php

namespace Thecodersprint\\PackageGenerator;

use Illuminate\\Support\\ServiceProvider;

class PackageGeneratorServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->publishes([
            __DIR__.'/../config/config.php' => config_path('package-generator.php'),
        ], 'config');
			
    }

    public function register()
    {
        //
    }
}

Step 4: Configuration File

Create a config.php file in the config folder:


<?php

return [
    // Configuration options
];

Step 5: Test the Package

Update the composer.json of your existing Laravel project to include your package:

"repositories": [
    {
        "type": "path",
        "url": "/path/to/package-generator",
        "options": {
            "symlink": true
        }
    }
],
"require": {
    "thecodersprint/package-generator": "dev-master"
}

  • repositories: This section tells Composer where to find the package.
    • type: "path" indicates the package is in a specific directory on your system.
    • url: This is the path to the package directory.
    • options: { "symlink": true } allows the package to be symlinked, so changes in the package directory will be reflected in the project immediately.
  • require: This section lists the packages your project depends on.
    • thecodersprint/package-generator: This is the name of your package.
    • "dev-master": This specifies the version of the package to use. "dev-master" means it will use the latest version from the master branch.

Run composer update to install the package.

Then, we will create a route to test if it is working properly:

Route::get('test', function() {
    return "Welcome to our package";
});

Access your-project.test/test to see the message "Welcome to our package."

Now we will try to publish our config file to check if the code is working.

php artisan vendor:publish

Then, we will select our service provider.

Step 6: Create a Command to Generate a Package

To create a command that generates the package boilerplate, we need to follow these steps:

  1. Create Stub Files
  2. Create the Command Class
  3. Create Directory Structure
  4. Create Essential Files
  5. Create Optional Components

6.1 Create Stub Files

We need to create stub files that will serve as templates for generating the package files.

If you don’t know about stubs, I'll explain. Stubs are template files used to generate code. They contain placeholders for elements like class names, namespaces, and configurations. When creating a new package, stubs help quickly generate the necessary files with the correct structure and content. This ensures consistency across all generated packages and saves time by automating repetitive tasks.

Create a resources/stubs directory and add the following stub files:

  • composer.stub
  • ServiceProvider.stub
  • MainClass.stub
  • Config.stub
  • Facade.stub
  • Controller.stub
  • Middleware.stub
  • LivewireComponent.stub
  • Routes.stub
  • BladeView.stub
  • Lang.stub
  • ExampleTest.stub

Here are examples of what these stub files might contain:

composer.stub

{
    "name": "{{ vendor }}/{{ name }}",
    "description": "A Laravel package",
    "autoload": {
        "psr-4": {
            "{{ vendor }}\\\\{{ name }}\\\\": "src/"
        }
    },
    "extra": {
        "laravel": {
            "providers": [
                "{{ vendor }}\\\\{{ name }}\\\\PackageServiceProvider"
            ]
        }
    },
    "require": {}
}

ServiceProvider.stub

<?php

namespace {{ vendor }}\\{{ name }};

use Illuminate\\Support\\ServiceProvider;

class PackageServiceProvider extends ServiceProvider
{
    public function boot()
    {
        $this->publishes([
            __DIR__.'/../config/config.php' => config_path('{{ name }}.php'),
        ], 'config');
    }

    public function register()
    {
        //
    }
}

MainClass.stub

<?php

namespace {{ vendor }}\\{{ name }};

class {{ name }}
{
    //
}

Config.stub

<?php

return [
    // Configuration options
];

Create similar stub files for the other components as needed.

6.2 Create the Command Class

To generate the package structure, we need to create a command class that encapsulates the package generation logic. This command will be responsible for creating the necessary directories and files for the package based on the provided vendor and package names.

First, create a new file named GeneratePackage.php in the src/Console/Commands directory with the following content:

<?php

namespace Thecodersprint\\\\PackageGenerator\\\\Console\\\\Commands;

use Illuminate\\\\Console\\\\Command;
use Illuminate\\\\Support\\\\Str;
use File;

class GeneratePackage extends Command
{
    // Define the command signature and description
    protected $signature = 'make:package {vendor} {name}';
    protected $description = 'Generate a Laravel package structure';

    // The handle method is the entry point for the command execution
    public function handle()
    {
        // Retrieve the vendor and package name arguments
        $vendor = $this->argument('vendor');
        $name = $this->argument('name');
        $packagePath = base_path("packages/{$vendor}/{$name}");

        // Inform the user that the package structure is being created
        $this->info("Creating package structure for {$vendor}/{$name}");

        // Create the necessary directories and files for the package
        $this->createDirectories($packagePath);
        $this->createFiles($packagePath, $vendor, $name);

        // Inform the user that the package has been successfully generated
        $this->info("Package {$vendor}/{$name} generated successfully.");
    }
}

In this GeneratePackage class:

  1. Command Definition:
    • The protected $signature defines how the command is called from the terminal, including the expected arguments (vendor and name).
    • The protected $description provides a brief description of what the command does.
  2. Handle Method:
    • The handle method is the main entry point for the command execution. When the command is run, this method is called.
    • It retrieves the vendor and name arguments provided by the user.
    • It constructs the path where the package will be created ($packagePath).
    • It then calls helper methods to create the necessary directories and files for the new package.
  3. Creating Directories and Files:
    • The createDirectories method (to be defined later) will handle the creation of the required directory structure.
    • The createFiles method (to be defined later) will handle the creation of the essential files using predefined stubs.

We will register our command in our service provider boot method.


// Register the command if we are running in the console
			if ($this->app->runningInConsole()) {
            $this->commands([
                \Thecodersprint\PackageGenerator\Console\Commands\GeneratePackage::class,
            ]);
        }

6.3 Create Directory Structure

Next, we need to create the directory structure for the package. Add the createDirectories method to the command class:

private function createDirectories($packagePath)
{
    $directories = [
        "{$packagePath}/config",
        "{$packagePath}/src/Facades",
        "{$packagePath}/src/Console/Commands",
        "{$packagePath}/src/Http/Controllers",
        "{$packagePath}/src/Http/Middleware",
        "{$packagePath}/src/Models",
        "{$packagePath}/src/Resources/lang/en",
        "{$packagePath}/src/Resources/views",
        "{$packagePath}/src/Routes",
        "{$packagePath}/tests/Feature",
        "{$packagePath}/tests/Unit",
    ];

    foreach ($directories as $dir) {
        if (!File::exists($dir)) {
            File::makeDirectory($dir, 0755, true);
        }
    }
}

This method creates the necessary directories for the package. It checks if each directory exists and creates it if it doesn't.

6.4 Create Essential Files

Now, we need to create the essential files for the package. Add the createFiles method to the command class:

private function createFiles($packagePath, $vendor, $name)
{
    $this->createFile("{$packagePath}/composer.json", $this->getStub('composer.stub', $vendor, $name));
    $this->createFile("{$packagePath}/src/PackageServiceProvider.php", $this->getStub('ServiceProvider.stub', $vendor, $name));
    $this->createFile("{$packagePath}/src/{$name}.php", $this->getStub('MainClass.stub', $vendor, $name));
    $this->createFile("{$packagePath}/config/config.php", $this->getStub('Config.stub'));
}

private function createFile($path, $content)
{
    File::put($path, $content);
}

private function getStub($stub, $vendor = null, $name = null)
{
    $stubPath = __DIR__ . "/../../../resources/stubs/{$stub}";
    $stubContent = File::get($stubPath);

    if ($vendor && $name) {
        $stubContent = str_replace(
            ['{{ vendor }}', '{{ name }}'],
            [$vendor, $name],
            $stubContent
        );
    }

    return $stubContent;
}

The createFiles method generates the essential files for the package using stub templates. The getStub method reads the stub file and replaces placeholders with the actual vendor and package names.

6.5 Create Optional Components

Next, we will prompt the user for additional components and generate them accordingly. Add the createOptionalComponents method to the command class:

private function createOptionalComponents($packagePath, $vendor, $name)
{
    if ($this->confirm('Do you need a facade?')) {
        $this->createFile("{$packagePath}/src/Facades/{$name}Facade.php", $this->getStub('Facade.stub', $vendor, $name));
    }

    if ($this->confirm('Do you need a controller?')) {
        $this->createFile("{$packagePath}/src/Http/Controllers/{$name}Controller.php", $this->getStub('Controller.stub', $vendor, $name));
    }

    if ($this->confirm('Do you need middleware?')) {
        $this->createFile("{$packagePath}/src/Http/Middleware/{$name}Middleware.php", $this->getStub('Middleware.stub', $vendor, $name));
    }

    if ($this->confirm('Do you need Livewire components?')) {
        $this->createFile("{$packagePath}/src/Http/Livewire/{$name}Component.php", $this->getStub('LivewireComponent.stub', $vendor, $name));
        $this->updateComposerJsonForLivewire($packagePath);
    }

    if ($this->confirm('Do you need routes?')) {
        $this->createFile("{$packagePath}/src/routes/web.php", $this->getStub('Routes.stub'));
    }

    if ($this->confirm('Do you need views?')) {
        $this->createFile("{$packagePath}/src/resources/views/example.blade.php", $this->getStub('BladeView.stub'));
    }

    if ($this->confirm('Do you need language files?')) {
        $this->createFile("{$packagePath}/src/resources/lang/en/messages.php", $this->getStub('Lang.stub'));
    }

    if ($this->confirm('Do you need tests?')) {
        $this->createFile("{$packagePath}/tests/Feature/ExampleTest.php", $this->getStub('ExampleTest.stub'));
    }
}

private function updateComposerJsonForLivewire($packagePath)
{
    $composerPath = "{$packagePath}/composer.json";
    $composerContent = json_decode(File::get($composerPath), true);

    $composerContent['require']['livewire/livewire'] = "^3.0";

    File::put($composerPath, json_encode($composerContent, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
}

This method prompts the user for each optional component and generates the necessary files if the user confirms.

Here is the complete class code:

<?php

namespace Thecodersprint\\PackageGenerator\\Console\\Commands;

use Illuminate\\Console\\Command;
use Illuminate\\Support\\Str;
use File;

class GeneratePackage extends Command
{
    protected $signature = 'make:package {vendor} {name}';
    protected $description = 'Generate a Laravel package structure';

    public function handle()
    {
        $vendor = $this->argument('vendor');
        $name = $this->argument('name');
        $packagePath = base_path("packages/{$vendor}/{$name}");

        $this->info("Creating package structure for {$vendor}/{$name}");

        // Create directories and files
        $this->createDirectories($packagePath);
        $this->createFiles($packagePath, $vendor, $name);
        $this->info("Package {$vendor}/{$name} generated successfully.");
    }

    private function createDirectories($packagePath)
    {
        $directories = [
            "{$packagePath}/config",
            "{$packagePath}/src/Facades",
            "{$packagePath}/src/Console/Commands",
            "{$packagePath}/src/Http/Controllers",
            "{$packagePath}/src/Http/Middleware",
            "{$packagePath}/src/Models",
            "{$packagePath}/src/Resources/lang/en",
            "{$packagePath}/src/Resources/views",
            "{$packagePath}/src/Routes",
            "{$packagePath}/tests/Feature",
            "{$packagePath}/tests/Unit",
        ];

        foreach ($directories as $dir) {
            if (!File::exists($dir)) {
                File::makeDirectory($dir, 0755, true);
            }
        }
    }

    private function createFiles($packagePath, $vendor, $name)
    {
        $this->createFile("{$packagePath}/composer.json", $this->getStub('composer.stub', $vendor, $name));
        $this->createFile("{$packagePath}/src/PackageServiceProvider.php", $this->getStub('ServiceProvider.stub', $vendor, $name));
        $this->createFile("{$packagePath}/src/{$name}.php", $this->getStub('MainClass.stub', $vendor, $name));
        $this->createFile("{$packagePath}/config/config.php", $this->getStub('Config.stub'));
    }

    private function createFile($path, $content)
    {
        File::put($path, $content);
    }

    private function getStub($stub, $vendor = null, $name = null)
    {
        $stubPath = __DIR__ . "/../../../resources/stubs/{$stub}";
        $stubContent = File::get($stubPath);

        if ($vendor && $name) {
            $stubContent = str_replace(
                ['{{ vendor }}', '{{ name }}'],
                [$vendor, $name],
                $stubContent
            );
        }

        return $stubContent;
    }
}

Step 7: Test the Command

Run the command to generate a new package:

php artisan make:package thecodersprint sample

Follow the prompts to customize your package structure. Here is the result of the above command:

Conclusion

By following these steps, you can create a Laravel package that automates repetitive tasks and improves your workflow. This example demonstrates the basics, but you can expand it to fit your specific needs.

Creating packages not only saves time but also ensures that your projects adhere to consistent standards. Happy coding!

user

Author: Manish Kumar

I've been working with PHP for over 11 years and focusing on Laravel for the last 6 years. During this time, I've taken part in big projects and gotten to know tools like Laravel Livewire really well. Besides working on projects, I enjoy writing about Laravel on LinkedIn and Medium to share what I've learned. I'm always looking to learn more and share my knowledge with others. Right now, I'm diving into new areas like using Docker to make web apps run better and exploring how to build them using microservices. My goal is to keep learning and help others do the same in the world of web development.

Leave a reply

dot

Subscribe to Newsletter

Provide your email to get email notification when we launch new products or publish new articles

Newsletter

Subscribe to my newsletter to get updated posts

Don't worry, I don't spam