jptagorda / laravel-package-starterkit
A Laravel package starterkit with action-based architecture, contracts, and strict conventions
Package info
github.com/jptagorda/laravel-package-starterkit
pkg:composer/jptagorda/laravel-package-starterkit
Requires
- php: ^8.4
- illuminate/contracts: ^12.0
- illuminate/support: ^12.0
Requires (Dev)
- laravel/pint: ^1.0
- orchestra/testbench: ^10.0
- pestphp/pest: ^3.0
README
A production-ready Laravel package template with action-based architecture, strict conventions, and comprehensive tooling.
Overview
This starterkit provides a solid foundation for building Laravel packages that follow best practices:
- Action-based architecture for predictable state mutations
- Contract-driven design for testability and flexibility
- Value objects for type-safe, immutable data
- Pest + Orchestra Testbench for robust testing
- Pre-configured tooling (Pint, Prettier, PHPUnit)
Requirements
- PHP 8.4+
- Laravel 12+
- Composer 2.0+
Installation
Install the starterkit in your Laravel application:
composer require jptagorda/laravel-package-starterkit --dev
Quick Start
Create a New Package
Use the Artisan command to scaffold a new package:
php artisan make:package vendor/package-name
Example:
php artisan make:package acme/billing
This creates a fully configured package at packages/acme/billing/ with:
- Service provider with auto-discovery
- Exception hierarchy
- Test infrastructure (Pest + Orchestra Testbench)
- Configuration file
- Documentation stubs
- Code style configs (Pint, Prettier)
Command Options
# Create a new package php artisan make:package acme/my-package # Overwrite existing package php artisan make:package acme/my-package --force
After Scaffolding
cd packages/acme/my-package composer install composer test
Add to Root composer.json
Add the package as a path repository:
{
"repositories": [
{
"type": "path",
"url": "packages/acme/my-package"
}
]
}
Then require it:
composer require acme/my-package
Generated Structure
packages/acme/my-package/
├── src/
│ ├── Actions/ # State mutation classes
│ ├── Contracts/ # Interface definitions
│ ├── Exceptions/ # Package-specific exceptions
│ │ ├── PackageException.php
│ │ ├── ValidationException.php
│ │ └── ConfigurationException.php
│ ├── ValueObjects/ # Immutable data containers
│ └── MyPackageServiceProvider.php
├── config/
│ └── my-package.php
├── tests/
│ ├── Feature/
│ │ └── ServiceProviderTest.php
│ ├── Unit/
│ ├── Pest.php
│ └── TestCase.php
├── .docs/
│ ├── index.md
│ ├── installation.md
│ ├── configuration.md
│ └── usage.md
├── composer.json
├── README.md
├── CHANGELOG.md
├── phpunit.xml.dist
├── pint.json
└── .prettierrc
Architecture Guidelines
Actions
All state mutations flow through Action classes:
<?php declare(strict_types=1); namespace Acme\MyPackage\Actions; final readonly class CreateEntityAction { public function __construct( private EntityRepositoryContract $repository, ) {} public function __invoke(EntityData $data): Entity { return $this->repository->create($data); } }
Contracts
Define behavior through interfaces:
<?php declare(strict_types=1); namespace Acme\MyPackage\Contracts; interface EntityRepositoryContract { public function find(int $id): ?Entity; public function create(EntityData $data): Entity; }
Value Objects
Immutable data with constructor validation:
<?php declare(strict_types=1); namespace Acme\MyPackage\ValueObjects; final readonly class Email { public function __construct( public string $value, ) { if (!filter_var($value, FILTER_VALIDATE_EMAIL)) { throw ValidationException::invalidField('email', 'Invalid email format'); } } }
Exceptions
Use specific, contextual exceptions:
use Acme\MyPackage\Exceptions\ValidationException; throw ValidationException::invalidField('email', 'Must be a valid email'); throw ValidationException::requiredField('name');
Package Commands
Inside your generated package:
| Command | Description |
|---|---|
composer test |
Run Pest tests |
composer format |
Format code with Pint |
Configuration
Generated config files follow these rules:
- All keys have explicit defaults (no nulls)
- Keys use
snake_case - Maximum 3 levels of nesting
- Config declares policy, not logic
Testing
Tests use Pest with Orchestra Testbench:
<?php declare(strict_types=1); it('creates entity with valid data', function (): void { $action = app(CreateEntityAction::class); $result = $action(new EntityData(name: 'Test')); expect($result)->toBeInstanceOf(Entity::class); }); it('throws exception for invalid data', function (): void { $action = app(CreateEntityAction::class); expect(fn () => $action(new EntityData(name: ''))) ->toThrow(ValidationException::class); });
Code Style
- PHP: Laravel Pint (PSR-12 + Laravel preset)
- JS/JSON/MD: Prettier
Run formatters:
composer format # PHP files npx prettier --write . # Other files
Alternative: Manual Setup
If you prefer to clone the starterkit directly:
git clone https://github.com/jptagorda/laravel-package-starterkit.git my-package
cd my-package
rm -rf .git
git init
Then manually update:
composer.json- package name, namespace- Service provider - rename and update namespace
- Config file - rename
- Test files - update namespaces
License
MIT