generated from theradcoza/Laravel-Docker-Dev-Template
Initial commit
This commit is contained in:
286
docs/audit-trail.md
Normal file
286
docs/audit-trail.md
Normal 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
238
docs/backup.md
Normal 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
263
docs/ci-cd.md
Normal 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
177
docs/error-logging.md
Normal 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
220
docs/filament-admin.md
Normal 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
263
docs/laravel-setup.md
Normal 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
313
docs/modules.md
Normal 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
275
docs/queues.md
Normal 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
286
docs/site-settings.md
Normal 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
386
docs/testing.md
Normal 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
|
||||
Reference in New Issue
Block a user