Claude
Skills
Sign in
Back

spatie-guidelines

Included with Lifetime
$97 forever

Spatie's coding guidelines and conventions. Use when writing PHP, Laravel, JavaScript, or Vue code for Spatie projects or packages. Covers code style, naming, routing, controllers, Blade, validation, Git workflow, package structure, testing (Pest), and service providers. Triggers include "follow Spatie guidelines", "Spatie style", "Spatie package", or any code review for Spatie packages/projects.

Web Dev

What this skill does


# Spatie Guidelines

Apply these guidelines when writing code for Spatie projects or contributing to Spatie packages.

**Core principle:** Write things the way Laravel intended. If there's a documented way, follow it. Deviate only with justification.

---

## PHP Style

### Type System
- Type properties, parameters, and return types. Skip docblocks for fully typed methods.
- Use `?Type` (short nullable), not `Type|null`.
- Use `void` return type when a method returns nothing.
- Use constructor property promotion when all properties can be promoted; one per line, trailing comma:

```php
class MyClass {
    public function __construct(
        protected string $firstArgument,
        protected string $secondArgument,
    ) {}
}
```

### Docblocks
- Skip docblocks for fully type-hinted methods unless you need a description.
- Use full sentences with a period for descriptions.
- Always import classnames in docblocks (use FQCNs like `\Spatie\Url\Url`).
- One-line docblocks when possible: `/** @var string */`
- Always add docblock types for iterables: `@param array<int, string> $items`
- If a function needs one docblock param, add all other params too.

### Code Style
- PSR-1, PSR-2, PSR-12.
- Don't use `final` by default.
- Prefer string interpolation: `"Hi, I am {$name}."`
- Enums use PascalCase values: `case Diamonds;`
- Each trait on its own line with its own `use`:

```php
class MyClass
{
    use TraitA;
    use TraitB;
}
```

### Control Flow
- **Happy path last** — handle failures first, return early.
- **Avoid `else`** — refactor to early returns or ternaries.
- **Separate compound ifs** — individual `if` statements over `&&` chains.
- Always use curly brackets for `if` statements.
- Whitespace: blank lines between statements (except sequences of single-line operations).
- No extra empty lines between `{}` brackets.

### Comments
- Avoid comments. Write expressive code instead.
- Refactor comments into descriptively named methods.

---

## Laravel Conventions

### Configuration
- Config filenames: **kebab-case** (`media-library.php`, `permission.php`)
- Config keys: **snake_case** (`'chrome_path' => env('CHROME_PATH')`)
- Never use `env()` outside config files.
- Service-specific config goes in `config/services.php`, not a new file.

### Routing
- URLs: **kebab-case** (`/open-source`, `/front-end-developer`)
- Route names: **camelCase** (`->name('openSource')`)
- Route parameters: **camelCase** (`{newsItem}`)
- HTTP verb first: `Route::get('open-source', [OpenSourceController::class, 'index'])`
- Use tuple notation: `[Controller::class, 'method']`, not string `'Controller@method'`
- Don't prefix URLs with `/` (except root `/`)

### API Routing
- Plural resource names: `/errors`, `/error-occurrences`
- Kebab-case resources
- Limit deep nesting. Prefer `/error-occurrences/1` over `/projects/1/errors/1/error-occurrences/1`
- Nest only when context is necessary: `/errors/1/occurrences`

### Controllers
- **Plural** resource name + `Controller` suffix: `PostsController`
- Stick to CRUD keywords: `index`, `create`, `store`, `show`, `edit`, `update`, `destroy`
- Extract new controllers for non-CRUD actions (e.g., `FavoritePostsController` with `store`/`destroy`)
- Invokable controllers for single actions: `PerformCleanupController`

### Views & Blade
- View files: **camelCase** (`openSource.blade.php`)
- Indent with 4 spaces.
- No spaces after directives: `@if($condition)`
- Use `__()` for translations, not `@lang`

### Validation
- Always array notation: `['required', 'email']`, never pipe `'required|email'`
- Custom rules: **snake_case** (`organisation_type`)

### Authorization
- Policies: **camelCase** (`editPost`)
- Use CRUD words; replace `show` with `view`

### Artisan Commands
- Command names: **kebab-case** (`delete-old-records`)
- Always output feedback. Minimum: `$this->comment('All ok!')` at end.
- For batch processing: output progress per item, summary at end.

### Naming Classes

| Type | Convention | Example |
|------|-----------|---------|
| Controller | Plural + `Controller` | `PostsController` |
| Invokable Controller | Action + `Controller` | `PerformCleanupController` |
| Model | Singular | `Post` |
| Job | Action description | `CreateUser` |
| Event | Tense indicates timing | `ApprovingLoan` / `LoanApproved` |
| Listener | Action + `Listener` | `SendInvitationMailListener` |
| Command | Action + `Command` | `PublishScheduledPostsCommand` |
| Mailable | Event/action + `Mail` | `AccountActivatedMail` |
| Resource | Plural + `Resource` | `UsersResource` |
| Enum | Descriptive, no prefix | `OrderStatus`, `Suit` |

---

## Spatie Package Architecture

### Package Structure (spatie/package-skeleton-laravel)

```
src/                          # Main source code
src/Commands/                 # Artisan commands
src/Components/               # Blade components
src/Contracts/                # Interfaces
src/Events/                   # Events
src/Exceptions/               # Exception classes
src/Models/                   # Eloquent models
src/Traits/                   # Reusable traits
config/                       # Config file (kebab-case, no `laravel-` prefix)
database/factories/           # Model factories
database/migrations/          # Migrations (stubs)
resources/views/              # Views
routes/                       # Routes
tests/                        # Tests (Pest)
```

### Service Provider Pattern

All Spatie Laravel packages extend `PackageServiceProvider` from `spatie/laravel-package-tools`:

```php
use Spatie\LaravelPackageTools\Package;
use Spatie\LaravelPackageTools\PackageServiceProvider;

class MediaLibraryServiceProvider extends PackageServiceProvider
{
    public function configurePackage(Package $package): void
    {
        $package
            ->name('laravel-medialibrary')     // Package name
            ->hasConfigFile('media-library')   // Config without 'laravel-' prefix
            ->hasMigration('create_media_table')
            ->hasViews('media-library')
            ->hasCommands([
                RegenerateCommand::class,
                ClearCommand::class,
                CleanCommand::class,
            ]);
    }

    public function packageBooted(): void
    {
        // Post-boot logic: observers, event listeners, gate registrations
    }

    public function packageRegistered(): void
    {
        // Bindings, singletons, scoped instances
    }
}
```

**Key lifecycle methods:**
- `configurePackage()` — declare assets (config, migrations, views, commands)
- `packageRegistered()` — bind interfaces, register singletons
- `packageBooted()` — register observers, macros, blade directives

### Namespace Conventions
- Root namespace: `Spatie\PackageName` (e.g., `Spatie\Permission`, `Spatie\MediaLibrary`)
- Packagist name: `spatie/laravel-package-name` or `spatie/package-name`
- Config file drops the `laravel-` prefix: `spatie/laravel-permission` → `config/permission.php`

### Contracts Pattern

Spatie packages use contracts (interfaces) for extensibility:

```php
// src/Contracts/Permission.php
namespace Spatie\Permission\Contracts;

interface Permission
{
    // ...
}

// src/Models/Permission.php — implements the contract
class Permission extends Model implements PermissionContract
{
    // ...
}

// Service provider binds contract to implementation
$this->app->bind(PermissionContract::class, 
    fn ($app) => $app->make($app->config['permission.models.permission'])
);
```

Config allows users to swap model implementations:

```php
// config/permission.php
return [
    'models' => [
        'permission' => Spatie\Permission\Models\Permission::class,
        'role' => Spatie\Permission\Models\Role::class,
    ],
    'table_names' => [
        'roles' => 'roles',
        'permissions' => 'permissions',
    ],
];
```

### Model Patterns
- Use `$guarded = []` (not `$fillable`).
- Configurable table names via config: `$this->table = config('permission.table_names.permissions') ?: parent::getTable();`
- Provide st

Related in Web Dev