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.
- Setting up the Package Skeleton
- Understanding the Package Structure
- Implementation of the Settings Package
- Testing Your Package
- Documentation
- Publishing Your Package
- Maintaining Your Package
- Composer installed
- Git installed
- GitHub account (optional but recommended)
- Use Composer to create a new package based on Spatie's skeleton:
composer create-project spatie/package-skeleton-laravel your-package-name- Navigate to the package directory:
cd your-package-name-
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
-
Initialize git and make your first commit:
git init
git add .
git commit -m "Initial commit"- 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 mainThe 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 resideconfig/: Configuration files that users can publishdatabase/migrations/: Migration files that users can publishtests/: All your test filescomposer.json: Defines your package dependencies and metadata
Let's implement our settings management package. We'll create the following components:
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');
}
}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',
];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'));
}
};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());
}
}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));
}
}
}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;
}
}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;
}
}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
],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',
]);
}
}Run your tests with:
composer testGood 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
[](https://packagist.org/packages/your-vendor/your-package-name)
[](https://github.com/your-vendor/your-package-name/actions?query=workflow%3Arun-tests+branch%3Amain)
[](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-nameYou can publish and run the migrations with:
php artisan vendor:publish --tag="your-package-name-migrations"
php artisan migrateYou can publish the config file with:
php artisan vendor:publish --tag="your-package-name-config"// 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');The configuration file allows you to customize:
- Cache behavior for settings
- Database table name
- Default settings values
// In a service provider or middleware
if (app('settings')->get('auto_clear_empty_inventory')) {
// Do something
}composer testPlease see CHANGELOG for more information on what has changed recently.
Please see CONTRIBUTING for details.
Please review our security policy on how to report security vulnerabilities.
The MIT License (MIT). Please see License File 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:
```json
"require": {
"php": "^8.0",
"illuminate/support": "^8.0|^9.0|^10.0"
},
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
- Be responsive to issues and pull requests
- Keep a CONTRIBUTING.md file with guidelines
- Maintain a CODE_OF_CONDUCT.md file
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:
- Solving a common problem
- Excellent documentation
- Comprehensive testing
- Active maintenance
Good luck with your package development journey!