Skip to content

Instantly share code, notes, and snippets.

@AHS12
Last active February 7, 2026 15:04
Show Gist options
  • Select an option

  • Save AHS12/2fd9ccf5f667e0f84abdfea37a4ea4aa to your computer and use it in GitHub Desktop.

Select an option

Save AHS12/2fd9ccf5f667e0f84abdfea37a4ea4aa to your computer and use it in GitHub Desktop.
How to create a Laravel Package

Complete Guide to Creating a Laravel Package

This guide will walk you through creating a Laravel package from scratch using Spatie's package skeleton. We'll create a settings management package, organized as a proper Laravel package that can be shared and reused.

Table of Contents

  1. Setting up the Package Skeleton
  2. Understanding the Package Structure
  3. Implementation of the Settings Package
  4. Testing Your Package
  5. Documentation
  6. Publishing Your Package
  7. Maintaining Your Package

1. Setting up the Package Skeleton

Prerequisites

  • Composer installed
  • Git installed
  • GitHub account (optional but recommended)

Creating a New Package

  1. Use Composer to create a new package based on Spatie's skeleton:
composer create-project spatie/package-skeleton-laravel your-package-name
  1. Navigate to the package directory:
cd your-package-name
  1. The skeleton will ask you several questions to set up your package:

    • Vendor name (typically your GitHub username or organization name)
    • Package name (e.g., "laravel-settings")
    • Package description
    • Author name and email
    • GitHub repository details
  2. Initialize git and make your first commit:

git init
git add .
git commit -m "Initial commit"
  1. If you have a GitHub account, create a repository and push your code:
git remote add origin https://github.com/your-username/your-package-name.git
git push -u origin main

2. Understanding the Package Structure

The skeleton creates a standard package structure:

.
├── .github/            # GitHub workflows and templates
├── config/             # Configuration files
├── database/           # Migrations
├── resources/          # Views, language files, etc.
├── src/                # Your PHP code
├── tests/              # Test files
├── .editorconfig       # Editor settings
├── .gitattributes      # Git attributes
├── .gitignore          # Git ignore rules
├── CHANGELOG.md        # Change log
├── composer.json       # Composer configuration
├── LICENSE.md          # License
├── README.md           # Documentation
├── phpunit.xml.dist    # PHPUnit configuration

Key files and directories:

  • src/: This is where all your main PHP code will reside
  • config/: Configuration files that users can publish
  • database/migrations/: Migration files that users can publish
  • tests/: All your test files
  • composer.json: Defines your package dependencies and metadata

3. Implementation of the Settings Package

Let's implement our settings management package. We'll create the following components:

Step 1: Define the Package Service Provider

Edit src/YourPackageNameServiceProvider.php:

<?php

namespace YourVendor\YourPackageName;

use Illuminate\Support\ServiceProvider;
use YourVendor\YourPackageName\Commands\RefreshSettingsCommand;
use YourVendor\YourPackageName\Services\SettingsService;

class YourPackageNameServiceProvider extends ServiceProvider
{
    public function configurePackage(Package $package): void
    {
        $package
            ->name('your-package-name')
            ->hasConfigFile()
            ->hasMigration('create_settings_table')
            ->hasCommand(RefreshSettingsCommand::class);
    }

    public function packageRegistered()
    {
        // Register the settings service in the container
        $this->app->singleton(SettingsService::class, function ($app) {
            return new SettingsService();
        });
        
        // Register an alias for easier access
        $this->app->alias(SettingsService::class, 'settings');
    }
}

Step 2: Create the Configuration File

Create config/your-package-name.php:

<?php

return [
    /*
    |--------------------------------------------------------------------------
    | Settings Cache
    |--------------------------------------------------------------------------
    |
    | This option controls whether the settings are cached.
    |
    */
    'cache' => [
        'enabled' => env('SETTINGS_CACHE_ENABLED', true),
        'ttl' => env('SETTINGS_CACHE_TTL', 3600), // 1 hour
    ],
    
    /*
    |--------------------------------------------------------------------------
    | Settings Table
    |--------------------------------------------------------------------------
    |
    | The database table used to store settings.
    |
    */
    'table' => 'settings',
];

Step 3: Create the Migration File

Create database/migrations/create_settings_table.php.stub:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create(config('your-package-name.table', 'settings'), function (Blueprint $table) {
            $table->id();
            $table->string('key')->unique();
            $table->text('value')->nullable();
            $table->text('description')->nullable();
            $table->string('type')->default('boolean');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists(config('your-package-name.table', 'settings'));
    }
};

Step 4: Create the Setting Model

Create src/Models/Setting.php:

<?php

namespace YourVendor\YourPackageName\Models;

use Illuminate\Database\Eloquent\Model;

class Setting extends Model
{
    protected $fillable = ['key', 'value', 'description', 'type'];
    
    public function getValueAttribute($value)
    {
        return match($this->type) {
            'boolean' => (bool) $value,
            'integer' => (int) $value,
            'json' => json_decode($value, true),
            default => $value,
        };
    }
    
    public function setValueAttribute($value)
    {
        if ($this->type === 'json' && is_array($value)) {
            $this->attributes['value'] = json_encode($value);
        } else {
            $this->attributes['value'] = $value;
        }
    }
    
    public function getTable()
    {
        return config('your-package-name.table', parent::getTable());
    }
}

Step 5: Create the Settings Service

Create src/Services/SettingsService.php:

<?php

namespace YourVendor\YourPackageName\Services;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use YourVendor\YourPackageName\Models\Setting;

class SettingsService
{
    protected Collection $settings;
    
    public function __construct()
    {
        $this->loadSettings();
    }
    
    protected function loadSettings(): void
    {
        $cacheEnabled = config('your-package-name.cache.enabled', true);
        $cacheTtl = config('your-package-name.cache.ttl', 3600);
        
        if ($cacheEnabled) {
            $this->settings = Cache::remember('settings', $cacheTtl, function () {
                return $this->getSettingsFromDatabase();
            });
        } else {
            $this->settings = $this->getSettingsFromDatabase();
        }
    }
    
    protected function getSettingsFromDatabase(): Collection
    {
        $settings = collect();
        Setting::all()->each(function ($setting) use ($settings) {
            $settings->put($setting->key, $setting->value);
        });
        
        return $settings;
    }
    
    public function get(string $key, $default = null)
    {
        return $this->settings->get($key, $default);
    }
    
    public function set(string $key, $value): void
    {
        $setting = Setting::firstOrCreate(['key' => $key]);
        $setting->value = $value;
        $setting->type = is_bool($value) ? 'boolean' : (is_int($value) ? 'integer' : (is_array($value) ? 'json' : 'string'));
        $setting->save();
        
        $this->settings->put($key, $value);
        
        if (config('your-package-name.cache.enabled', true)) {
            Cache::put('settings', $this->settings, config('your-package-name.cache.ttl', 3600));
        }
    }
    
    public function all(): Collection
    {
        return $this->settings;
    }
    
    public function has(string $key): bool
    {
        return $this->settings->has($key);
    }
    
    public function forget(string $key): void
    {
        Setting::where('key', $key)->delete();
        $this->settings->forget($key);
        
        if (config('your-package-name.cache.enabled', true)) {
            Cache::put('settings', $this->settings, config('your-package-name.cache.ttl', 3600));
        }
    }
}

Step 6: Create a Facade for Easy Access

Create src/Facades/Settings.php:

<?php

namespace YourVendor\YourPackageName\Facades;

use Illuminate\Support\Facades\Facade;
use YourVendor\YourPackageName\Services\SettingsService;

/**
 * @method static mixed get(string $key, mixed $default = null)
 * @method static void set(string $key, mixed $value)
 * @method static \Illuminate\Support\Collection all()
 * @method static bool has(string $key)
 * @method static void forget(string $key)
 * 
 * @see \YourVendor\YourPackageName\Services\SettingsService
 */
class Settings extends Facade
{
    protected static function getFacadeAccessor()
    {
        return SettingsService::class;
    }
}

Step 7: Create the Command

Create src/Commands/RefreshSettingsCommand.php:

<?php

namespace YourVendor\YourPackageName\Commands;

use Illuminate\Console\Command;
use YourVendor\YourPackageName\Models\Setting;

class RefreshSettingsCommand extends Command
{
    protected $signature = 'settings:refresh {--force : Force update of all settings to default values}';
    protected $description = 'Refresh settings structure';

    public function handle()
    {
        $settings = config('your-package-name.default_settings', []);
        $force = $this->option('force');
        
        foreach ($settings as $key => $default) {
            $setting = Setting::firstOrCreate(['key' => $key]);
            
            if ($setting->wasRecentlyCreated || $force) {
                $setting->value = $default['value'] ?? null;
                $this->info("Setting '{$key}' value set to default");
            }
            
            $setting->description = $default['description'] ?? '';
            $setting->type = $default['type'] ?? 'string';
            $setting->save();
            
            if ($setting->wasRecentlyCreated) {
                $this->info("Setting '{$key}' created.");
            } else {
                $this->line("Setting '{$key}' updated.");
            }
        }
        
        $this->info('Settings refresh completed!');
        
        return Command::SUCCESS;
    }
}

Step 8: Update Package-Specific Config

Update the config file to include default settings:

// In config/your-package-name.php add:

'default_settings' => [
    'auto_clear_empty_inventory' => [
        'value' => true,
        'description' => 'Automatically remove inventory items when quantity reaches zero',
        'type' => 'boolean',
    ],
    // Add more default settings as needed
],

4. Testing Your Package

Writing Tests

The skeleton comes with a basic test setup. Let's add some tests for our settings package:

Create tests/SettingsServiceTest.php:

<?php

namespace YourVendor\YourPackageName\Tests;

use YourVendor\YourPackageName\Facades\Settings;
use YourVendor\YourPackageName\Models\Setting;

class SettingsServiceTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();
        
        // Migrate the settings table
        $this->artisan('migrate');
    }
    
    /** @test */
    public function it_can_get_and_set_settings()
    {
        // Set a setting
        Settings::set('test_key', 'test_value');
        
        // Check it was saved to the database
        $this->assertDatabaseHas('settings', [
            'key' => 'test_key',
        ]);
        
        // Check we can retrieve it
        $this->assertEquals('test_value', Settings::get('test_key'));
    }
    
    /** @test */
    public function it_handles_different_types_of_settings()
    {
        // Boolean
        Settings::set('boolean_setting', true);
        $this->assertIsBool(Settings::get('boolean_setting'));
        $this->assertTrue(Settings::get('boolean_setting'));
        
        // Integer
        Settings::set('integer_setting', 123);
        $this->assertIsInt(Settings::get('integer_setting'));
        $this->assertEquals(123, Settings::get('integer_setting'));
        
        // JSON/Array
        $array = ['key' => 'value', 'nested' => ['data' => true]];
        Settings::set('json_setting', $array);
        $this->assertEquals($array, Settings::get('json_setting'));
    }
    
    /** @test */
    public function it_returns_default_value_when_setting_not_found()
    {
        $this->assertEquals('default', Settings::get('nonexistent', 'default'));
    }
    
    /** @test */
    public function it_can_forget_settings()
    {
        Settings::set('temporary', 'value');
        $this->assertTrue(Settings::has('temporary'));
        
        Settings::forget('temporary');
        $this->assertFalse(Settings::has('temporary'));
        $this->assertDatabaseMissing('settings', [
            'key' => 'temporary',
        ]);
    }
}

Running Tests

Run your tests with:

composer test

5. Documentation

Good documentation is crucial for package adoption. Update your README.md with:

  • Installation instructions
  • Configuration details
  • Usage examples
  • API documentation
  • Contributing guidelines

Here's a sample structure for your README:

# Your Package Name

[![Latest Version on Packagist](https://img.shields.io/packagist/v/your-vendor/your-package-name.svg?style=flat-square)](https://packagist.org/packages/your-vendor/your-package-name)
[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/your-vendor/your-package-name/run-tests?label=tests)](https://github.com/your-vendor/your-package-name/actions?query=workflow%3Arun-tests+branch%3Amain)
[![Total Downloads](https://img.shields.io/packagist/dt/your-vendor/your-package-name.svg?style=flat-square)](https://packagist.org/packages/your-vendor/your-package-name)

A Laravel package for managing application settings with database storage and caching.

## Installation

You can install the package via composer:

```bash
composer require your-vendor/your-package-name
```

You can publish and run the migrations with:

```bash
php artisan vendor:publish --tag="your-package-name-migrations"
php artisan migrate
```

You can publish the config file with:

```bash
php artisan vendor:publish --tag="your-package-name-config"
```

## Usage

```php
// Get a setting with a default fallback value
$value = Settings::get('auto_clear_empty_inventory', true);

// Set a setting
Settings::set('auto_clear_empty_inventory', false);

// Check if a setting exists
if (Settings::has('feature_x')) {
    // Do something
}

// Get all settings
$allSettings = Settings::all();

// Remove a setting
Settings::forget('temporary_feature');
```

## Configuration

The configuration file allows you to customize:

- Cache behavior for settings
- Database table name
- Default settings values

```php
// In a service provider or middleware
if (app('settings')->get('auto_clear_empty_inventory')) {
    // Do something
}
```

## Testing

```bash
composer test
```

## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

## Security Vulnerabilities

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

## Credits

- [Your Name](https://github.com/yourusername)
- [All Contributors](../../contributors)

## License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

6. Publishing Your Package

Preparing for Release

  1. Make sure all tests pass
  2. Update your README.md with complete documentation
  3. Update the CHANGELOG.md file
  4. Check that your composer.json is complete and accurate

Publishing to Packagist

  1. Create a release on GitHub
  2. Submit your package to Packagist: https://packagist.org/packages/submit
  3. Connect your Packagist account to GitHub for automatic updates

7. Maintaining Your Package

Version Control

Follow semantic versioning:

  • MAJOR version for incompatible API changes
  • MINOR version for new functionality that is backward-compatible
  • PATCH version for backward-compatible bug fixes

Supporting Multiple Laravel Versions

In your composer.json, you can specify Laravel version compatibility:

"require": {
    "php": "^8.0",
    "illuminate/support": "^8.0|^9.0|^10.0"
},

Continuous Integration

Use GitHub Actions (already set up in the skeleton) to:

  • Run tests on multiple PHP and Laravel versions
  • Check code style
  • Generate code coverage reports

Handling Community Contributions

  • Be responsive to issues and pull requests
  • Keep a CONTRIBUTING.md file with guidelines
  • Maintain a CODE_OF_CONDUCT.md file

Conclusion

Creating a Laravel package allows you to share your code with the community and use it across multiple projects. This settings package is a perfect example of functionality that can be extracted into a reusable component.

By following the structure and principles outlined in this guide, you'll create a well-organized, tested, and documented package that follows Laravel best practices.

Remember that the key factors for package adoption are:

  1. Solving a common problem
  2. Excellent documentation
  3. Comprehensive testing
  4. Active maintenance

Good luck with your package development journey!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment