Initial commit

This commit is contained in:
2026-03-09 09:18:21 +00:00
commit b4355fee17
191 changed files with 25537 additions and 0 deletions

286
docs/audit-trail.md Normal file
View File

@@ -0,0 +1,286 @@
# Audit Trail
This template includes a comprehensive audit trail system using [owen-it/laravel-auditing](https://github.com/owen-it/laravel-auditing) with Filament UI integration.
## What Gets Tracked
For every audited model:
- **Who** - User who made the change
- **What** - Model and record ID
- **When** - Timestamp
- **Changes** - Old values → New values
- **Where** - IP address, user agent
- **Module** - Which module the change belongs to
## Quick Start
Audit trail is set up during `make setup-laravel`. To add auditing to a model:
```php
use App\Traits\ModuleAuditable;
use OwenIt\Auditing\Contracts\Auditable;
class Product extends Model implements Auditable
{
use ModuleAuditable;
// Your model code...
}
```
That's it! All create, update, and delete operations are now logged.
## Viewing Audit Logs
Each module has an **Audit Log** page in its admin section:
```
📦 Stock Management
├── Dashboard
├── Products
└── Audit Log ← Click here
```
The audit log shows:
- Date/Time
- User
- Event type (created/updated/deleted)
- Model
- Old → New values
Click any entry to see full details including IP address and all changed fields.
## Configuration
### Per-Module Configuration
Each module has audit settings in its config file:
```php
// Config/stock_management.php
'audit' => [
'enabled' => true, // Enable/disable for entire module
'strategy' => 'all', // 'all', 'include', 'exclude', 'none'
'exclude' => [ // Fields to never audit
'password',
'remember_token',
],
],
```
### Strategies
| Strategy | Description |
|----------|-------------|
| `all` | Audit all fields (default) |
| `include` | Only audit fields in `$auditInclude` |
| `exclude` | Audit all except fields in `$auditExclude` |
| `none` | Disable auditing |
### Per-Model Configuration
Override in your model:
```php
class Product extends Model implements Auditable
{
use ModuleAuditable;
// Only audit these fields
protected $auditInclude = [
'name',
'price',
'quantity',
];
// Or exclude specific fields
protected $auditExclude = [
'internal_notes',
'cache_data',
];
}
```
## Audit Events
By default, these events are tracked:
- `created` - New record created
- `updated` - Record modified
- `deleted` - Record deleted
### Custom Events
Log custom events:
```php
// In your model or service
$product->auditEvent = 'approved';
$product->isCustomEvent = true;
$product->auditCustomOld = ['status' => 'pending'];
$product->auditCustomNew = ['status' => 'approved'];
$product->save();
```
## Querying Audits
### Get audits for a record
```php
$product = Product::find(1);
$audits = $product->audits;
// With user info
$audits = $product->audits()->with('user')->get();
```
### Get audits by user
```php
use OwenIt\Auditing\Models\Audit;
$userAudits = Audit::where('user_id', $userId)->get();
```
### Get audits by module
```php
$moduleAudits = Audit::where('tags', 'like', '%module:StockManagement%')->get();
```
### Get recent changes
```php
$recentChanges = Audit::latest()->take(50)->get();
```
## UI Customization
### Modify Audit Log Table
Edit `Filament/Resources/AuditLogResource.php` in your module:
```php
public static function table(Table $table): Table
{
return $table
->columns([
// Add or modify columns
Tables\Columns\TextColumn::make('custom_field'),
])
->filters([
// Add custom filters
Tables\Filters\Filter::make('today')
->query(fn ($query) => $query->whereDate('created_at', today())),
]);
}
```
### Add Audit Tab to Resource
Add audit history tab to any Filament resource:
```php
use Tapp\FilamentAuditing\RelationManagers\AuditsRelationManager;
class ProductResource extends Resource
{
public static function getRelations(): array
{
return [
AuditsRelationManager::class,
];
}
}
```
## Data Retention
### Pruning Old Audits
Add to `app/Console/Kernel.php`:
```php
protected function schedule(Schedule $schedule)
{
// Delete audits older than 90 days
$schedule->command('audit:prune --days=90')->daily();
}
```
Or run manually:
```bash
php artisan audit:prune --days=90
```
### Archive Before Delete
```php
// Export to CSV before pruning
Audit::where('created_at', '<', now()->subDays(90))
->each(function ($audit) {
// Write to archive file
Storage::append('audits/archive.csv', $audit->toJson());
});
```
## Disabling Auditing
### Temporarily
```php
// Disable for a single operation
$product->disableAuditing();
$product->update(['price' => 99.99]);
$product->enableAuditing();
// Or use without auditing
Product::withoutAuditing(function () {
Product::where('category', 'sale')->update(['on_sale' => true]);
});
```
### For Specific Model
```php
class CacheModel extends Model implements Auditable
{
use ModuleAuditable;
// Disable auditing for this model
public $auditEvents = [];
}
```
### For Entire Module
```php
// Config/module_name.php
'audit' => [
'enabled' => false,
],
```
## Troubleshooting
### Audits not being created
1. Model implements `Auditable` interface
2. Model uses `ModuleAuditable` trait
3. Check module config `audit.enabled` is true
4. Run `php artisan config:clear`
### User not being recorded
1. Ensure user is authenticated when changes are made
2. Check `config/audit.php` for user resolver settings
### Performance concerns
1. Use `$auditInclude` to limit tracked fields
2. Set up audit pruning for old records
3. Consider async audit processing for high-volume apps
## Security
- Audit records are **read-only** in the admin panel
- No create/edit/delete actions available
- Access controlled by module permissions
- Sensitive fields (password, tokens) excluded by default

238
docs/backup.md Normal file
View File

@@ -0,0 +1,238 @@
# Database Backup & Restore
Scripts for backing up and restoring your database.
## Quick Start
```bash
# Create backup
make backup
# List backups
ls -la backups/
# Restore from backup
make restore file=backups/laravel_20240306_120000.sql.gz
```
## Backup
The backup script automatically:
- Detects database type (MySQL, PostgreSQL, SQLite)
- Creates timestamped backup
- Compresses with gzip
- Keeps only last 10 backups
### Manual Backup
```bash
./scripts/backup.sh
```
Output:
```
==========================================
Database Backup
==========================================
Connection: mysql
Database: laravel
Creating MySQL backup...
✓ Backup created successfully!
File: backups/laravel_20240306_120000.sql.gz
Size: 2.5M
Recent backups:
-rw-r--r-- 1 user user 2.5M Mar 6 12:00 laravel_20240306_120000.sql.gz
-rw-r--r-- 1 user user 2.4M Mar 5 12:00 laravel_20240305_120000.sql.gz
```
### Backup Location
```
backups/
├── laravel_20240306_120000.sql.gz
├── laravel_20240305_120000.sql.gz
└── laravel_20240304_120000.sql.gz
```
## Restore
```bash
# With make
make restore file=backups/laravel_20240306_120000.sql.gz
# Or directly
./scripts/restore.sh backups/laravel_20240306_120000.sql.gz
```
**Warning:** Restore will overwrite the current database!
## Automated Backups
### Using Scheduler
Add to your Laravel scheduler (`routes/console.php`):
```php
use Illuminate\Support\Facades\Schedule;
// Daily backup at 2 AM
Schedule::exec('bash scripts/backup.sh')
->dailyAt('02:00')
->sendOutputTo(storage_path('logs/backup.log'));
```
### Using Cron (Production)
```bash
# Edit crontab
crontab -e
# Add daily backup at 2 AM
0 2 * * * cd /var/www/html && bash scripts/backup.sh >> /var/log/laravel-backup.log 2>&1
```
## Remote Backup Storage
### Copy to S3
```bash
# Install AWS CLI
pip install awscli
# Configure
aws configure
# Upload backup
LATEST=$(ls -t backups/*.gz | head -1)
aws s3 cp "$LATEST" s3://your-bucket/backups/
```
### Automate S3 Upload
Add to `scripts/backup.sh`:
```bash
# After backup creation
if command -v aws &> /dev/null; then
echo "Uploading to S3..."
aws s3 cp "$BACKUP_FILE" "s3://${S3_BUCKET}/backups/"
fi
```
### Using Laravel Backup Package
For more features, use [spatie/laravel-backup](https://github.com/spatie/laravel-backup):
```bash
composer require spatie/laravel-backup
php artisan vendor:publish --provider="Spatie\Backup\BackupServiceProvider"
```
```php
// config/backup.php
'destination' => [
'disks' => ['local', 's3'],
],
// Schedule
Schedule::command('backup:run')->daily();
Schedule::command('backup:clean')->daily();
```
## Database-Specific Notes
### MySQL
```bash
# Manual backup
docker-compose exec mysql mysqldump -u laravel -p laravel > backup.sql
# Manual restore
docker-compose exec -T mysql mysql -u laravel -p laravel < backup.sql
```
### PostgreSQL
```bash
# Manual backup
docker-compose exec pgsql pg_dump -U laravel laravel > backup.sql
# Manual restore
docker-compose exec -T pgsql psql -U laravel laravel < backup.sql
```
### SQLite
```bash
# Manual backup (just copy the file)
cp src/database/database.sqlite backups/database_backup.sqlite
# Manual restore
cp backups/database_backup.sqlite src/database/database.sqlite
```
## Backup Strategy
### Development
- Manual backups before major changes
- Keep last 5 backups
### Staging
- Daily automated backups
- Keep last 7 days
### Production
- Hourly incremental (if supported)
- Daily full backup
- Weekly backup to offsite storage
- Keep 30 days of backups
- Test restores monthly
## Testing Restores
**Important:** Regularly test your backups!
```bash
# Create test database
docker-compose exec mysql mysql -u root -p -e "CREATE DATABASE restore_test;"
# Restore to test database
gunzip -c backups/latest.sql.gz | docker-compose exec -T mysql mysql -u root -p restore_test
# Verify data
docker-compose exec mysql mysql -u root -p restore_test -e "SELECT COUNT(*) FROM users;"
# Cleanup
docker-compose exec mysql mysql -u root -p -e "DROP DATABASE restore_test;"
```
## Troubleshooting
### Backup fails with permission error
```bash
chmod +x scripts/backup.sh scripts/restore.sh
mkdir -p backups
chmod 755 backups
```
### Restore fails - database locked
```bash
# Stop queue workers
make queue-stop
# Run restore
make restore file=backups/backup.sql.gz
# Restart queue workers
make queue-start
```
### Large database backup timeout
```bash
# Increase timeout in docker-compose.yml
environment:
MYSQL_CONNECT_TIMEOUT: 600
```

263
docs/ci-cd.md Normal file
View File

@@ -0,0 +1,263 @@
# CI/CD Pipeline
This template includes a GitHub Actions workflow for continuous integration and deployment.
## Overview
```
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Push to │────▶│ Run Tests │────▶│ Deploy Staging │
│ develop │ │ + Lint │ │ (automatic) │
└─────────────┘ └─────────────┘ └─────────────────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────────┐
│ Push to │────▶│ Run Tests │────▶│ Deploy Prod │
│ main │ │ + Lint │ │ (with approval)│
└─────────────┘ └─────────────┘ └─────────────────┘
```
## Workflow File
Located at `.github/workflows/ci.yml`
### Jobs
| Job | Trigger | Description |
|-----|---------|-------------|
| **tests** | All pushes/PRs | Run Pest tests + Pint lint |
| **deploy-staging** | Push to `develop` | Auto-deploy to staging |
| **deploy-production** | Push to `main` | Deploy with approval |
## Setup
### 1. Create GitHub Secrets
Go to: Repository → Settings → Secrets and variables → Actions
**For Staging:**
```
STAGING_HOST - Staging server IP/hostname
STAGING_USER - SSH username
STAGING_SSH_KEY - Private SSH key (full content)
```
**For Production:**
```
PRODUCTION_HOST - Production server IP/hostname
PRODUCTION_USER - SSH username
PRODUCTION_SSH_KEY - Private SSH key (full content)
```
### 2. Generate SSH Key
```bash
# Generate a new key pair
ssh-keygen -t ed25519 -C "github-actions" -f github-actions-key
# Add public key to server
cat github-actions-key.pub >> ~/.ssh/authorized_keys
# Copy private key to GitHub secret
cat github-actions-key
```
### 3. Configure Server
On your server, ensure:
```bash
# Create deployment directory
sudo mkdir -p /var/www/staging
sudo mkdir -p /var/www/production
sudo chown -R $USER:www-data /var/www/staging /var/www/production
# Clone repository
cd /var/www/staging
git clone git@github.com:your-repo.git .
# Install dependencies
composer install
npm install && npm run build
# Configure environment
cp .env.example .env
php artisan key:generate
# Edit .env with production values
# Set permissions
chmod -R 775 storage bootstrap/cache
```
### 4. Environment Protection (Optional)
For production deployments with approval:
1. Go to Repository → Settings → Environments
2. Create `production` environment
3. Enable "Required reviewers"
4. Add team members who can approve
## Workflow Customization
### Add More Tests
```yaml
- name: Run security audit
working-directory: ./src
run: composer audit
- name: Run static analysis
working-directory: ./src
run: ./vendor/bin/phpstan analyse
```
### Add Notifications
```yaml
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,commit,author,action
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
if: always()
```
### Add Database Migrations Check
```yaml
- name: Check pending migrations
working-directory: ./src
run: |
PENDING=$(php artisan migrate:status | grep -c "No" || true)
if [ "$PENDING" -gt 0 ]; then
echo "::warning::There are pending migrations"
fi
```
## Manual Deployment
If you prefer manual deployments:
```bash
# On your server
cd /var/www/production
# Enable maintenance mode
php artisan down
# Pull latest code
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear and cache
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Restart queue workers
php artisan queue:restart
# Disable maintenance mode
php artisan up
```
## Deployment Script
Create `deploy/scripts/deploy.sh` for reusable deployment:
```bash
#!/bin/bash
set -e
echo "🚀 Starting deployment..."
# Enter maintenance mode
php artisan down
# Pull latest changes
git pull origin main
# Install dependencies
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Build assets
npm ci
npm run build
# Clear caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Restart queue
php artisan queue:restart
# Exit maintenance mode
php artisan up
echo "✅ Deployment complete!"
```
## Rollback
If deployment fails:
```bash
# Revert to previous commit
git reset --hard HEAD~1
# Or specific commit
git reset --hard <commit-hash>
# Re-run caching
php artisan config:cache
php artisan route:cache
# Restart services
php artisan queue:restart
php artisan up
```
## Testing Locally
Test the CI workflow locally with [act](https://github.com/nektos/act):
```bash
# Install act
brew install act # macOS
# or
curl https://raw.githubusercontent.com/nektos/act/master/install.sh | sudo bash
# Run tests job
act -j tests
# Run with secrets
act -j tests --secret-file .secrets
```
## Troubleshooting
### SSH Connection Failed
- Verify SSH key is correct (no extra newlines)
- Check server firewall allows port 22
- Ensure key is added to `~/.ssh/authorized_keys`
### Permission Denied
- Check file ownership: `chown -R www-data:www-data /var/www`
- Check directory permissions: `chmod -R 775 storage bootstrap/cache`
### Composer/NPM Fails
- Ensure sufficient memory on server
- Check PHP extensions are installed
- Verify Node.js version matches requirements

177
docs/error-logging.md Normal file
View File

@@ -0,0 +1,177 @@
# Error Logging Setup
This template uses **Flare + Ignition** by Spatie for error logging.
## Overview
| Environment | Tool | Purpose |
|-------------|------|---------|
| Development | **Ignition** | Rich in-browser error pages with AI explanations |
| Development | **Telescope** (optional) | Request/query/job debugging dashboard |
| Production | **Flare** | Remote error tracking, notifications |
## Architecture
```
Development:
Error → Ignition → Beautiful error page with:
- Stack trace with code context
- AI-powered explanations
- Click-to-open in VS Code
- Suggested solutions
Production:
Error → Flare (remote) → Notifications (Slack/Email)
User sees clean 500.blade.php error page
```
## Setup
### 1. Run Post-Install Script
After creating your Laravel project:
```bash
# In Docker
make setup-tools
# Or manually
cd src
bash ../scripts/post-install.sh
```
### 2. Get Flare API Key
1. Sign up at [flareapp.io](https://flareapp.io)
2. Create a new project
3. Copy your API key
### 3. Configure Environment
**Development (.env):**
```env
FLARE_KEY=your_flare_key_here
IGNITION_THEME=auto
IGNITION_EDITOR=vscode
```
**Production (.env):**
```env
APP_DEBUG=false
FLARE_KEY=your_flare_key_here
```
## Ignition Features (Development)
### AI Error Explanations
Ignition can explain errors using AI. Click "AI" button on any error page.
### Click-to-Open in Editor
Errors link directly to the file and line in your editor.
Supported editors (set via `IGNITION_EDITOR`):
- `vscode` - Visual Studio Code
- `phpstorm` - PhpStorm
- `sublime` - Sublime Text
- `atom` - Atom
- `textmate` - TextMate
### Runnable Solutions
Ignition suggests fixes for common issues that you can apply with one click.
### Share Error Context
Click "Share" to create a shareable link for debugging with teammates.
## Telescope (Optional)
Telescope provides a debug dashboard at `/telescope` with:
- **Requests** - All HTTP requests with timing
- **Exceptions** - All caught exceptions
- **Logs** - Log entries
- **Queries** - Database queries with timing
- **Jobs** - Queue job processing
- **Mail** - Sent emails
- **Notifications** - All notifications
- **Cache** - Cache operations
### Installing Telescope
The post-install script offers to install Telescope. To install manually:
```bash
composer require laravel/telescope --dev
php artisan telescope:install
php artisan migrate
```
### Telescope in Production
Telescope is installed as a dev dependency. For production debugging:
1. Install without `--dev`
2. Configure authorization in `app/Providers/TelescopeServiceProvider.php`
3. Access via `/telescope` (requires authentication)
## Custom Error Pages
The post-install script creates custom error pages:
- `resources/views/errors/404.blade.php` - Not Found
- `resources/views/errors/500.blade.php` - Server Error
- `resources/views/errors/503.blade.php` - Maintenance Mode
These are shown to users in production while Flare captures the full error details.
## Flare Dashboard
In your Flare dashboard you can:
- View all errors with full stack traces
- See request data, session, user info
- Group errors by type
- Track error frequency over time
- Set up notifications (Slack, Email, Discord)
- Mark errors as resolved
## Testing Error Logging
### Test in Development
Add a test route in `routes/web.php`:
```php
Route::get('/test-error', function () {
throw new \Exception('Test error for Flare!');
});
```
Visit `/test-error` to see:
- Ignition error page (development)
- Error logged in Flare dashboard
### Test in Production
```bash
php artisan down # Enable maintenance mode
# Visit site - should see 503 page
php artisan up # Disable maintenance mode
```
## Troubleshooting
### Errors not appearing in Flare
1. Check `FLARE_KEY` is set correctly
2. Verify `APP_ENV=production` and `APP_DEBUG=false`
3. Check network connectivity from server
### Ignition not showing AI explanations
1. Requires OpenAI API key in Flare settings
2. Available on paid Flare plans
### Telescope not loading
1. Run `php artisan telescope:install`
2. Run `php artisan migrate`
3. Clear cache: `php artisan config:clear`

220
docs/filament-admin.md Normal file
View File

@@ -0,0 +1,220 @@
# Filament Admin Panel
This template includes optional [Filament](https://filamentphp.com/) admin panel setup for user management and administration.
## What is Filament?
Filament is a full-stack admin panel framework for Laravel built on Livewire. It provides:
- **Admin Panel** - Beautiful, responsive dashboard
- **Form Builder** - Dynamic forms with validation
- **Table Builder** - Sortable, searchable, filterable tables
- **User Management** - CRUD for users out of the box
- **Widgets** - Dashboard stats and charts
- **Notifications** - Toast notifications
- **Actions** - Bulk actions, row actions
## Installation
Filament is installed via `make setup-laravel` when you select "Yes" for admin panel.
Manual installation:
```bash
composer require filament/filament:"^3.2" -W
php artisan filament:install --panels
php artisan make:filament-user
php artisan make:filament-resource User --generate
```
## Accessing Admin Panel
- **URL**: `http://localhost:8080/admin`
- **Login**: Use credentials created during setup
## User Management
The setup creates a `UserResource` at `app/Filament/Resources/UserResource.php`.
This provides:
- List all users with search/filter
- Create new users
- Edit existing users
- Delete users
### Customizing User Resource
Edit `app/Filament/Resources/UserResource.php`:
```php
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('name')
->required()
->maxLength(255),
Forms\Components\TextInput::make('email')
->email()
->required()
->unique(ignoreRecord: true),
Forms\Components\DateTimePicker::make('email_verified_at'),
Forms\Components\TextInput::make('password')
->password()
->dehydrateStateUsing(fn ($state) => Hash::make($state))
->dehydrated(fn ($state) => filled($state))
->required(fn (string $context): bool => $context === 'create'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('name')
->searchable(),
Tables\Columns\TextColumn::make('email')
->searchable(),
Tables\Columns\TextColumn::make('email_verified_at')
->dateTime()
->sortable(),
Tables\Columns\TextColumn::make('created_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
//
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
]);
}
```
## Adding Roles & Permissions
For role-based access, add Spatie Permission:
```bash
composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
```
Then install Filament Shield for admin UI:
```bash
composer require bezhansalleh/filament-shield
php artisan shield:install
```
This adds:
- Role management in admin
- Permission management
- Protect resources by role
## Creating Additional Resources
```bash
# Generate resource for a model
php artisan make:filament-resource Post --generate
# Generate with soft deletes
php artisan make:filament-resource Post --generate --soft-deletes
# Generate simple (modal-based, no separate pages)
php artisan make:filament-resource Post --simple
```
## Dashboard Widgets
Create a stats widget:
```bash
php artisan make:filament-widget StatsOverview --stats-overview
```
Edit `app/Filament/Widgets/StatsOverview.php`:
```php
protected function getStats(): array
{
return [
Stat::make('Total Users', User::count()),
Stat::make('Verified Users', User::whereNotNull('email_verified_at')->count()),
Stat::make('New Today', User::whereDate('created_at', today())->count()),
];
}
```
## Restricting Admin Access
### By Email Domain
In `app/Providers/Filament/AdminPanelProvider.php`:
```php
->authGuard('web')
->login()
->registration(false) // Disable public registration
```
### By User Method
Add to `User` model:
```php
public function canAccessPanel(Panel $panel): bool
{
return str_ends_with($this->email, '@yourcompany.com');
}
```
Or with roles:
```php
public function canAccessPanel(Panel $panel): bool
{
return $this->hasRole('admin');
}
```
## Customizing Theme
Publish and customize:
```bash
php artisan vendor:publish --tag=filament-config
php artisan vendor:publish --tag=filament-panels-translations
```
Edit colors in `app/Providers/Filament/AdminPanelProvider.php`:
```php
->colors([
'primary' => Color::Indigo,
'danger' => Color::Rose,
'success' => Color::Emerald,
])
```
## Production Considerations
1. **Disable registration** in admin panel
2. **Use strong passwords** for admin users
3. **Enable 2FA** if using Jetstream
4. **Restrict by IP** in production if possible
5. **Monitor admin actions** via activity logging
## Resources
- [Filament Documentation](https://filamentphp.com/docs)
- [Filament Plugins](https://filamentphp.com/plugins)
- [Filament Shield (Roles)](https://github.com/bezhanSalleh/filament-shield)

263
docs/laravel-setup.md Normal file
View File

@@ -0,0 +1,263 @@
# Laravel Base Setup Guide
This guide covers setting up authentication, API, and base middleware for your Laravel application.
## Quick Start
After installing Laravel and running `make setup-tools`, run:
```bash
make setup-laravel
```
This interactive script will:
1. Set up authentication (Breeze or Jetstream)
2. Configure Sanctum for API authentication
3. Create security middleware
4. Set up storage symlink
## Authentication Options
> **This template focuses on Blade and Livewire** - no JavaScript frameworks (Vue/React/Inertia). This keeps debugging simple and server-side.
### Laravel Breeze + Blade (Recommended)
Best for: Most applications. Simple, fast, easy to debug.
Features:
- Login, registration, password reset
- Email verification
- Profile editing
- Tailwind CSS styling
```bash
composer require laravel/breeze --dev
php artisan breeze:install blade
php artisan migrate
npm install && npm run build
```
### Laravel Breeze + Livewire
Best for: Apps needing reactive UI without JavaScript frameworks.
Same features as Blade, but with dynamic updates via Livewire.
```bash
composer require laravel/breeze --dev
php artisan breeze:install livewire
php artisan migrate
npm install && npm run build
```
### Laravel Breeze API Only
Best for: When you want to build your own Blade views.
Provides API authentication endpoints, you build the frontend.
```bash
composer require laravel/breeze --dev
php artisan breeze:install api
php artisan migrate
```
### Laravel Jetstream + Livewire (Full-featured)
Best for: SaaS applications needing teams, 2FA, API tokens.
Features:
- Profile management with photo upload
- Two-factor authentication
- API token management
- Team management (optional)
- Session management
- Browser session logout
```bash
composer require laravel/jetstream
php artisan jetstream:install livewire --teams
php artisan migrate
npm install && npm run build
```
## API Authentication (Sanctum)
Laravel Sanctum provides:
- SPA authentication (cookie-based)
- API token authentication
- Mobile app authentication
### Creating Tokens
```php
// Create a token
$token = $user->createToken('api-token')->plainTextToken;
// Create with abilities
$token = $user->createToken('api-token', ['posts:read', 'posts:write'])->plainTextToken;
```
### Authenticating Requests
```bash
# Using token
curl -H "Authorization: Bearer YOUR_TOKEN" https://your-app.com/api/user
# Using cookie (SPA)
# First get CSRF token from /sanctum/csrf-cookie
```
### Token Abilities
```php
// Check ability
if ($user->tokenCan('posts:write')) {
// Can write posts
}
// In route middleware
Route::post('/posts', [PostController::class, 'store'])
->middleware('ability:posts:write');
```
## Security Middleware
The setup script creates two middleware files:
### ForceHttps
Redirects HTTP to HTTPS in production.
```php
// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\ForceHttps::class);
})
```
### SecurityHeaders
Adds security headers to all responses:
- X-Frame-Options
- X-Content-Type-Options
- X-XSS-Protection
- Referrer-Policy
- Permissions-Policy
- Strict-Transport-Security (production only)
```php
// Register in bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
$middleware->append(\App\Http\Middleware\SecurityHeaders::class);
})
```
## API Routes Template
An example API routes file is provided at `src/routes/api.example.php`.
Key patterns:
- Health check endpoint
- Protected routes with `auth:sanctum`
- Token management endpoints
- API versioning structure
## CORS Configuration
If your API is consumed by a separate frontend:
1. Copy `src/config/cors.php.example` to `config/cors.php`
2. Update `allowed_origins` with your frontend URL
3. Set `FRONTEND_URL` in `.env`
```env
FRONTEND_URL=https://your-frontend.com
```
## Development Workflow
### After Setup
```bash
# Start development
make up DB=mysql
# Run migrations
make artisan cmd='migrate'
# Create a user (tinker)
make tinker
# User::factory()->create(['email' => 'test@example.com'])
# Run tests
make test
# Fix code style
make lint
```
### Common Tasks
```bash
# Create controller
make artisan cmd='make:controller Api/PostController --api'
# Create model with migration
make artisan cmd='make:model Post -m'
# Create form request
make artisan cmd='make:request StorePostRequest'
# Create resource
make artisan cmd='make:resource PostResource'
# Create policy
make artisan cmd='make:policy PostPolicy --model=Post'
```
## Testing API
### With curl
```bash
# Register (if using Breeze API)
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@test.com","password":"password","password_confirmation":"password"}'
# Login
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"password"}'
# Use token
curl http://localhost:8080/api/user \
-H "Authorization: Bearer YOUR_TOKEN"
```
### With Postman/Insomnia
1. Import the API collection (create from routes)
2. Set base URL to `http://localhost:8080/api`
3. Add Authorization header with Bearer token
## Troubleshooting
### CORS Errors
1. Check `config/cors.php` includes your frontend origin
2. Ensure `supports_credentials` is `true` if using cookies
3. Clear config cache: `php artisan config:clear`
### 401 Unauthorized
1. Check token is valid and not expired
2. Ensure `auth:sanctum` middleware is applied
3. For SPA: ensure CSRF cookie is set
### Session Issues
1. Check `SESSION_DOMAIN` matches your domain
2. For subdomains, use `.yourdomain.com`
3. Ensure Redis is running for session storage

313
docs/modules.md Normal file
View File

@@ -0,0 +1,313 @@
# Modular Architecture
This template uses a modular architecture to organize features into self-contained modules. Each module has its own admin panel, routes, views, and permissions.
## Quick Start
```bash
# Create a new module
php artisan make:module StockManagement
# Create with a model
php artisan make:module StockManagement --model=Product
# Create with API routes
php artisan make:module StockManagement --api
# Skip Filament admin
php artisan make:module StockManagement --no-filament
```
## Module Structure
```
app/Modules/StockManagement/
├── Config/
│ └── stock_management.php # Module configuration
├── Database/
│ ├── Migrations/ # Module-specific migrations
│ └── Seeders/
│ └── StockManagementPermissionSeeder.php
├── Filament/
│ └── Resources/ # Admin panel resources
│ ├── StockManagementDashboardResource.php
│ └── ProductResource.php # If --model used
├── Http/
│ ├── Controllers/
│ │ └── StockManagementController.php
│ ├── Middleware/
│ └── Requests/
├── Models/
│ └── Product.php # If --model used
├── Policies/
├── Services/ # Business logic
├── Routes/
│ ├── web.php # Frontend routes
│ └── api.php # API routes (if --api)
├── Resources/
│ ├── views/
│ │ ├── index.blade.php # Module landing page
│ │ ├── layouts/
│ │ └── filament/
│ ├── css/
│ │ └── stock-management.css # Module-specific styles
│ └── lang/en/
├── Permissions.php # Module permissions
└── StockManagementServiceProvider.php
```
## How Modules Work
### Auto-Loading
The `ModuleServiceProvider` automatically:
- Discovers all modules in `app/Modules/`
- Registers each module's service provider
- Loads routes, views, migrations, and translations
### Routes
Module routes are prefixed and named automatically:
```php
// Routes/web.php
Route::prefix('stock-management')
->name('stock-management.')
->middleware(['web', 'auth'])
->group(function () {
Route::get('/', [StockManagementController::class, 'index'])
->name('index');
});
```
Access: `http://localhost:8080/stock-management`
### Views
Module views use a namespace based on the module slug:
```php
// In controller
return view('stock-management::index');
// In Blade
@include('stock-management::partials.header')
```
### Filament Admin
Each module gets its own navigation group in the admin panel:
```
📦 Stock Management
├── Dashboard
├── Products
└── Inventory
```
Resources are automatically discovered from `Filament/Resources/`.
## Permissions
### Defining Permissions
Each module has a `Permissions.php` file:
```php
// app/Modules/StockManagement/Permissions.php
return [
'stock_management.view' => 'View Stock Management',
'stock_management.create' => 'Create stock records',
'stock_management.edit' => 'Edit stock records',
'stock_management.delete' => 'Delete stock records',
'stock_management.export' => 'Export stock data',
];
```
### Seeding Permissions
After creating a module, run:
```bash
php artisan db:seed --class=PermissionSeeder
```
This registers all module permissions and assigns them to the admin role.
### Using Permissions
In Blade:
```blade
@can('stock_management.view')
<a href="{{ route('stock-management.index') }}">Stock Management</a>
@endcan
```
In Controllers:
```php
public function index()
{
$this->authorize('stock_management.view');
// ...
}
```
In Filament Resources:
```php
public static function canAccess(): bool
{
return auth()->user()?->can('stock_management.view') ?? false;
}
```
## Module Assets
### App-Wide CSS/JS
Use the main `resources/css/app.css` and `resources/js/app.js` for shared styles.
### Module-Specific Styles
Each module has its own CSS file at `Resources/css/{module-slug}.css`.
Include in Blade:
```blade
@push('module-styles')
<link href="{{ asset('modules/stock-management/css/stock-management.css') }}" rel="stylesheet">
@endpush
```
Or inline in the view:
```blade
@push('module-styles')
<style>
.stock-table { /* ... */ }
</style>
@endpush
```
## Creating Models
### With Module Command
```bash
php artisan make:module StockManagement --model=Product
```
Creates:
- `Models/Product.php`
- Migration in `Database/Migrations/`
- Filament resource with CRUD pages
### Adding Models Later
```bash
# From project root
php artisan make:model Modules/StockManagement/Models/Inventory -m
```
Then create the Filament resource:
```bash
php artisan make:filament-resource Inventory \
--model=App\\Modules\\StockManagement\\Models\\Inventory
```
Move the resource to your module's `Filament/Resources/` directory.
## Example: Stock Management Module
### 1. Create the Module
```bash
php artisan make:module StockManagement --model=Product --api
```
### 2. Edit the Migration
```php
// Database/Migrations/xxxx_create_products_table.php
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('sku')->unique();
$table->text('description')->nullable();
$table->decimal('price', 10, 2);
$table->integer('quantity')->default(0);
$table->timestamps();
});
```
### 3. Update the Model
```php
// Models/Product.php
protected $fillable = [
'name',
'sku',
'description',
'price',
'quantity',
];
protected $casts = [
'price' => 'decimal:2',
];
```
### 4. Update Filament Resource
```php
// Filament/Resources/ProductResource.php
public static function form(Form $form): Form
{
return $form->schema([
Forms\Components\TextInput::make('name')->required(),
Forms\Components\TextInput::make('sku')->required()->unique(ignoreRecord: true),
Forms\Components\Textarea::make('description'),
Forms\Components\TextInput::make('price')->numeric()->prefix('$'),
Forms\Components\TextInput::make('quantity')->numeric()->default(0),
]);
}
```
### 5. Run Migrations & Seed
```bash
php artisan migrate
php artisan db:seed --class=PermissionSeeder
```
### 6. Access
- Frontend: `http://localhost:8080/stock-management`
- Admin: `http://localhost:8080/admin` → Stock Management section
## Best Practices
1. **Keep modules independent** - Avoid tight coupling between modules
2. **Use services** - Put business logic in `Services/` not controllers
3. **Define clear permissions** - One permission per action
4. **Use policies** - For complex authorization rules
5. **Module-specific migrations** - Keep data schema with the module
6. **Test modules** - Create tests in `tests/Modules/ModuleName/`
## Troubleshooting
### Module not loading
1. Check service provider exists and is named correctly
2. Clear cache: `php artisan config:clear && php artisan cache:clear`
3. Check `ModuleServiceProvider` is in `bootstrap/providers.php`
### Views not found
1. Verify view namespace matches module slug (kebab-case)
2. Check views are in `Resources/views/`
### Permissions not working
1. Run `php artisan db:seed --class=PermissionSeeder`
2. Clear permission cache: `php artisan permission:cache-reset`
3. Verify user has role with permissions
### Filament resources not showing
1. Check resource is in `Filament/Resources/`
2. Verify `canAccess()` returns true for your user
3. Clear Filament cache: `php artisan filament:cache-components`

275
docs/queues.md Normal file
View File

@@ -0,0 +1,275 @@
# Queues & Background Jobs
This template includes Redis-powered queues for background processing.
## Quick Start
```bash
# Start queue worker
make queue-start
# View queue logs
make queue-logs
# Stop queue worker
make queue-stop
# Restart after code changes
make queue-restart
```
## Configuration
Queue is configured to use Redis in `.env`:
```env
QUEUE_CONNECTION=redis
REDIS_HOST=redis
REDIS_PORT=6379
```
## Creating Jobs
```bash
php artisan make:job ProcessOrder
```
```php
<?php
namespace App\Jobs;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public Order $order
) {}
public function handle(): void
{
// Process the order
$this->order->update(['status' => 'processing']);
// Send notification, generate invoice, etc.
}
public function failed(\Throwable $exception): void
{
// Handle failure - log, notify admin, etc.
logger()->error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage(),
]);
}
}
```
## Dispatching Jobs
```php
// Dispatch immediately to queue
ProcessOrder::dispatch($order);
// Dispatch with delay
ProcessOrder::dispatch($order)->delay(now()->addMinutes(5));
// Dispatch to specific queue
ProcessOrder::dispatch($order)->onQueue('orders');
// Dispatch after response sent
ProcessOrder::dispatchAfterResponse($order);
// Chain jobs
Bus::chain([
new ProcessOrder($order),
new SendOrderConfirmation($order),
new NotifyWarehouse($order),
])->dispatch();
```
## Job Queues
Use different queues for different priorities:
```php
// High priority
ProcessPayment::dispatch($payment)->onQueue('high');
// Default
SendEmail::dispatch($email)->onQueue('default');
// Low priority
GenerateReport::dispatch($report)->onQueue('low');
```
Run workers for specific queues:
```bash
# Process high priority first
php artisan queue:work --queue=high,default,low
```
## Scheduled Jobs
Add to `app/Console/Kernel.php` or `routes/console.php`:
```php
// routes/console.php (Laravel 11+)
use Illuminate\Support\Facades\Schedule;
Schedule::job(new CleanupOldRecords)->daily();
Schedule::job(new SendDailyReport)->dailyAt('08:00');
Schedule::job(new ProcessPendingOrders)->everyFiveMinutes();
// With queue
Schedule::job(new GenerateBackup)->daily()->onQueue('backups');
```
Start scheduler:
```bash
make scheduler-start
```
## Module Jobs
When creating module jobs, place them in the module's directory:
```
app/Modules/Inventory/
├── Jobs/
│ ├── SyncStock.php
│ └── GenerateInventoryReport.php
```
```php
<?php
namespace App\Modules\Inventory\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SyncStock implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $tries = 3;
public $backoff = [60, 300, 900]; // Retry after 1min, 5min, 15min
public function handle(): void
{
// Sync stock levels
}
}
```
## Job Middleware
Rate limit jobs:
```php
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProcessOrder implements ShouldQueue
{
public function middleware(): array
{
return [
new RateLimited('orders'),
new WithoutOverlapping($this->order->id),
];
}
}
```
## Monitoring
### View Failed Jobs
```bash
php artisan queue:failed
```
### Retry Failed Jobs
```bash
# Retry specific job
php artisan queue:retry <job-id>
# Retry all failed jobs
php artisan queue:retry all
```
### Clear Failed Jobs
```bash
php artisan queue:flush
```
## Production Setup
For production, use Supervisor instead of Docker:
```ini
# /etc/supervisor/conf.d/laravel-worker.conf
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600
```
```bash
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
```
## Testing Jobs
```php
use Illuminate\Support\Facades\Queue;
it('dispatches order processing job', function () {
Queue::fake();
$order = Order::factory()->create();
// Trigger action that dispatches job
$order->markAsPaid();
Queue::assertPushed(ProcessOrder::class, function ($job) use ($order) {
return $job->order->id === $order->id;
});
});
it('processes order correctly', function () {
$order = Order::factory()->create(['status' => 'pending']);
// Run job synchronously
(new ProcessOrder($order))->handle();
expect($order->fresh()->status)->toBe('processing');
});
```

286
docs/site-settings.md Normal file
View File

@@ -0,0 +1,286 @@
# Site Settings
Manage your site's appearance (logo, favicon, colors) from the admin panel.
## Overview
Site settings are stored in the database using [spatie/laravel-settings](https://github.com/spatie/laravel-settings) and managed via Filament.
**Admin Location:** `/admin` → Settings → Appearance
## Available Settings
| Setting | Type | Description |
|---------|------|-------------|
| `site_name` | String | Site name (used in title, emails) |
| `logo` | Image | Header logo |
| `favicon` | Image | Browser favicon (32x32) |
| `primary_color` | Color | Primary brand color |
| `secondary_color` | Color | Secondary/accent color |
| `dark_mode` | Boolean | Enable dark mode toggle |
| `footer_text` | Text | Footer copyright text |
## Usage in Blade Templates
### Helper Functions
```blade
{{-- Site name --}}
<h1>{{ site_name() }}</h1>
{{-- Logo with fallback --}}
@if(site_logo())
<img src="{{ site_logo() }}" alt="{{ site_name() }}">
@else
<span>{{ site_name() }}</span>
@endif
{{-- Favicon --}}
<link rel="icon" href="{{ site_favicon() }}">
{{-- Colors --}}
<div style="background: {{ primary_color() }}">Primary</div>
<div style="background: {{ secondary_color() }}">Secondary</div>
{{-- Get any setting --}}
{{ site_settings('footer_text') }}
{{ site_settings('dark_mode') ? 'Dark' : 'Light' }}
```
### Site Head Component
Include in your layout's `<head>`:
```blade
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{-- This adds title, favicon, and CSS variables --}}
<x-site-head :title="$title ?? null" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
```
The component automatically:
- Sets the page title with site name
- Adds favicon link
- Creates CSS custom properties for colors
### CSS Custom Properties
After including `<x-site-head />`, these CSS variables are available:
```css
:root {
--primary-color: #3b82f6;
--secondary-color: #64748b;
}
```
Use in your CSS:
```css
.button {
background-color: var(--primary-color);
}
.text-accent {
color: var(--secondary-color);
}
```
Or with Tailwind arbitrary values:
```html
<button class="bg-[var(--primary-color)] text-white">
Click Me
</button>
```
### Utility Classes
The component also creates utility classes:
```html
<div class="bg-primary text-white">Primary background</div>
<div class="text-primary">Primary text</div>
<div class="border-primary">Primary border</div>
<div class="bg-secondary">Secondary background</div>
<button class="btn-primary">Styled Button</button>
```
## Usage in PHP
```php
use App\Settings\SiteSettings;
// Via dependency injection
public function __construct(private SiteSettings $settings)
{
$name = $this->settings->site_name;
}
// Via helper
$name = site_settings('site_name');
$logo = site_logo();
// Via app container
$settings = app(SiteSettings::class);
$color = $settings->primary_color;
```
## Customizing the Settings Page
Edit `app/Filament/Pages/ManageSiteSettings.php`:
### Add New Settings
1. Add property to `app/Settings/SiteSettings.php`:
```php
class SiteSettings extends Settings
{
// ... existing properties
public ?string $contact_email;
public ?string $phone_number;
}
```
2. Create migration in `database/settings/`:
```php
return new class extends SettingsMigration
{
public function up(): void
{
$this->migrator->add('site.contact_email', null);
$this->migrator->add('site.phone_number', null);
}
};
```
3. Add to form in `ManageSiteSettings.php`:
```php
Forms\Components\Section::make('Contact')
->schema([
Forms\Components\TextInput::make('contact_email')
->email(),
Forms\Components\TextInput::make('phone_number')
->tel(),
]),
```
4. Run migration: `php artisan migrate`
### Add Social Links
```php
// In SiteSettings.php
public ?array $social_links;
// In migration
$this->migrator->add('site.social_links', []);
// In form
Forms\Components\Repeater::make('social_links')
->schema([
Forms\Components\Select::make('platform')
->options([
'facebook' => 'Facebook',
'twitter' => 'Twitter/X',
'instagram' => 'Instagram',
'linkedin' => 'LinkedIn',
]),
Forms\Components\TextInput::make('url')
->url(),
])
->columns(2),
```
## Caching
Settings are cached automatically. Clear cache after direct database changes:
```bash
php artisan cache:clear
```
Or in code:
```php
app(SiteSettings::class)->refresh();
```
## File Storage
Logo and favicon are stored in `storage/app/public/site/`.
Make sure the storage link exists:
```bash
php artisan storage:link
```
## Example: Complete Layout
```blade
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<x-site-head :title="$title ?? null" />
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
{{-- Header with logo --}}
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto px-4 py-4">
@if(site_logo())
<img src="{{ site_logo() }}" alt="{{ site_name() }}" class="h-10">
@else
<span class="text-xl font-bold text-primary">{{ site_name() }}</span>
@endif
</div>
</header>
{{-- Content --}}
<main>
{{ $slot }}
</main>
{{-- Footer --}}
<footer class="bg-gray-800 text-white py-8">
<div class="max-w-7xl mx-auto px-4 text-center">
{!! site_settings('footer_text') ?? '© ' . date('Y') . ' ' . site_name() !!}
</div>
</footer>
</div>
</body>
</html>
```
## Troubleshooting
### Settings not updating
1. Clear cache: `php artisan cache:clear`
2. Check file permissions on storage directory
### Logo not showing
1. Run `php artisan storage:link`
2. Check logo exists in `storage/app/public/site/`
### Colors not applying
1. Make sure `<x-site-head />` is in your layout's `<head>`
2. Check browser dev tools for CSS variable values

386
docs/testing.md Normal file
View File

@@ -0,0 +1,386 @@
# Testing with Pest
This template uses [Pest](https://pestphp.com/) for testing - a modern PHP testing framework with elegant syntax.
## Quick Start
```bash
# Run all tests
make test
# Run with coverage
make test-coverage
# Run specific module tests
make test-module module=Inventory
# Run filtered tests
make test-filter filter="can create"
# Run in parallel (faster)
make test-parallel
```
## Test Structure
```
tests/
├── Feature/ # HTTP/integration tests
│ └── ExampleTest.php
├── Unit/ # Isolated unit tests
│ └── ExampleTest.php
├── Modules/ # Module-specific tests
│ └── Inventory/
│ ├── InventoryTest.php
│ └── ProductTest.php
├── Pest.php # Global configuration
├── TestCase.php # Base test class
└── TestHelpers.php # Custom helpers
```
## Writing Tests
### Basic Syntax
```php
// Simple test
it('has a welcome page', function () {
$this->get('/')->assertStatus(200);
});
// With description
test('users can login', function () {
$user = User::factory()->create();
$this->post('/login', [
'email' => $user->email,
'password' => 'password',
])->assertRedirect('/dashboard');
});
```
### Expectations
```php
it('can create a product', function () {
$product = Product::create(['name' => 'Widget', 'price' => 99.99]);
expect($product)
->toBeInstanceOf(Product::class)
->name->toBe('Widget')
->price->toBe(99.99);
});
```
### Grouped Tests
```php
describe('Product Model', function () {
it('can be created', function () {
// ...
});
it('has a price', function () {
// ...
});
it('belongs to a category', function () {
// ...
});
});
```
### Setup/Teardown
```php
beforeEach(function () {
$this->user = User::factory()->create();
});
afterEach(function () {
// Cleanup if needed
});
it('has a user', function () {
expect($this->user)->toBeInstanceOf(User::class);
});
```
## Global Helpers
Defined in `tests/Pest.php`:
```php
// Create a regular user
$user = createUser();
$user = createUser(['name' => 'John']);
// Create an admin user
$admin = createAdmin();
```
## Module Testing
When you create a module with `php artisan make:module`, tests are auto-generated:
```
tests/Modules/Inventory/
├── InventoryTest.php # Route and permission tests
└── ProductTest.php # Model tests (if --model used)
```
### Example Module Test
```php
<?php
use App\Models\User;
use App\Modules\Inventory\Models\Product;
beforeEach(function () {
$this->user = User::factory()->create();
$this->user->givePermissionTo([
'inventory.view',
'inventory.create',
]);
});
describe('Inventory Module', function () {
it('allows authenticated users to view page', function () {
$this->actingAs($this->user)
->get('/inventory')
->assertStatus(200);
});
it('redirects guests to login', function () {
$this->get('/inventory')
->assertRedirect('/login');
});
});
describe('Product Model', function () {
it('can be created', function () {
$product = Product::create([
'name' => 'Test Product',
'price' => 29.99,
]);
expect($product->name)->toBe('Test Product');
});
it('is audited on create', function () {
$this->actingAs($this->user);
$product = Product::create(['name' => 'Audited']);
$this->assertDatabaseHas('audits', [
'auditable_type' => Product::class,
'auditable_id' => $product->id,
'event' => 'created',
]);
});
});
```
## Testing Helpers
Defined in `tests/TestHelpers.php`:
```php
use Tests\TestHelpers;
uses(TestHelpers::class)->in('Modules');
// In your test
it('allows users with permission', function () {
$user = $this->userWithPermissions(['inventory.view']);
$this->actingAs($user)
->get('/inventory')
->assertStatus(200);
});
it('allows module access', function () {
$user = $this->userWithModuleAccess('inventory');
// User has view, create, edit, delete permissions
});
it('audits changes', function () {
$product = Product::create(['name' => 'Test']);
$this->assertAudited($product, 'created');
});
```
## Testing Livewire Components
```php
use Livewire\Livewire;
use App\Livewire\CreateProduct;
it('can create product via Livewire', function () {
$this->actingAs(createUser());
Livewire::test(CreateProduct::class)
->set('name', 'New Product')
->set('price', 49.99)
->call('save')
->assertHasNoErrors()
->assertRedirect('/products');
$this->assertDatabaseHas('products', [
'name' => 'New Product',
]);
});
it('validates required fields', function () {
Livewire::test(CreateProduct::class)
->set('name', '')
->call('save')
->assertHasErrors(['name' => 'required']);
});
```
## Testing Filament Resources
```php
use App\Modules\Inventory\Models\Product;
use function Pest\Livewire\livewire;
it('can list products in admin', function () {
$user = createAdmin();
$products = Product::factory()->count(5)->create();
$this->actingAs($user)
->get('/admin/inventory/products')
->assertSuccessful()
->assertSeeText($products->first()->name);
});
it('can create product from admin', function () {
$user = createAdmin();
livewire(\App\Modules\Inventory\Filament\Resources\ProductResource\Pages\CreateProduct::class)
->fillForm([
'name' => 'Admin Product',
'price' => 100,
])
->call('create')
->assertHasNoFormErrors();
$this->assertDatabaseHas('products', [
'name' => 'Admin Product',
]);
});
```
## Database Testing
Tests use SQLite in-memory for speed. The `LazilyRefreshDatabase` trait only runs migrations when needed.
### Factories
```php
// Create model
$product = Product::factory()->create();
// Create multiple
$products = Product::factory()->count(5)->create();
// With attributes
$product = Product::factory()->create([
'name' => 'Special Product',
'price' => 999.99,
]);
// With state
$product = Product::factory()->active()->create();
```
### Assertions
```php
// Database has record
$this->assertDatabaseHas('products', [
'name' => 'Widget',
]);
// Database missing record
$this->assertDatabaseMissing('products', [
'name' => 'Deleted Product',
]);
// Count records
$this->assertDatabaseCount('products', 5);
```
## HTTP Testing
```php
// GET request
$this->get('/products')->assertStatus(200);
// POST with data
$this->post('/products', [
'name' => 'New Product',
])->assertRedirect('/products');
// JSON API
$this->getJson('/api/products')
->assertStatus(200)
->assertJsonCount(5, 'data');
// With auth
$this->actingAs($user)
->get('/admin')
->assertStatus(200);
// Assert view
$this->get('/products')
->assertViewIs('products.index')
->assertViewHas('products');
```
## Running Specific Tests
```bash
# Single file
php artisan test tests/Feature/ExampleTest.php
# Single test by name
php artisan test --filter="can create product"
# Module tests only
php artisan test tests/Modules/Inventory
# With verbose output
php artisan test --verbose
# Stop on first failure
php artisan test --stop-on-failure
```
## Coverage
```bash
# Generate coverage report
make test-coverage
# Requires Xdebug or PCOV
# Coverage report saved to coverage/
```
## Tips
1. **Use factories** - Don't manually create models in tests
2. **Test behavior, not implementation** - Focus on what, not how
3. **One assertion per test** - Keep tests focused
4. **Use descriptive names** - `it('shows error when email is invalid')`
5. **Test edge cases** - Empty values, boundaries, errors
6. **Run tests often** - Before commits, after changes