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-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.- Make sure all tests pass
- Update your README.md with complete documentation
- Update the CHANGELOG.md file
- Check that your composer.json is complete and accurate
- Create a release on GitHub
- Submit your package to Packagist: https://packagist.org/packages/submit
- Connect your Packagist account to GitHub for automatic updates
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
In your composer.json, you can specify Laravel version compatibility:
"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!