Files
michaelschiemer/docs/guides/common-workflows.md
Michael Schiemer 36ef2a1e2c
Some checks failed
🚀 Build & Deploy Image / Determine Build Necessity (push) Failing after 10m14s
🚀 Build & Deploy Image / Build Runtime Base Image (push) Has been skipped
🚀 Build & Deploy Image / Build Docker Image (push) Has been skipped
🚀 Build & Deploy Image / Run Tests & Quality Checks (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Staging (push) Has been skipped
🚀 Build & Deploy Image / Auto-deploy to Production (push) Has been skipped
Security Vulnerability Scan / Check for Dependency Changes (push) Failing after 11m25s
Security Vulnerability Scan / Composer Security Audit (push) Has been cancelled
fix: Gitea Traefik routing and connection pool optimization
- Remove middleware reference from Gitea Traefik labels (caused routing issues)
- Optimize Gitea connection pool settings (MAX_IDLE_CONNS=30, authentication_timeout=180s)
- Add explicit service reference in Traefik labels
- Fix intermittent 504 timeouts by improving PostgreSQL connection handling

Fixes Gitea unreachability via git.michaelschiemer.de
2025-11-09 14:46:15 +01:00

1260 lines
28 KiB
Markdown

# Common Development Workflows
Standard workflows for common development tasks in the Custom PHP Framework.
## Adding a New Feature
### Step 1: Planning & Design
**Define the Feature**:
```bash
# Document feature requirements
# - What problem does it solve?
# - What are the acceptance criteria?
# - What are the security implications?
# - What is the performance impact?
```
**Design Decision Checklist**:
- [ ] Does this fit framework architecture?
- [ ] Are there existing patterns to follow?
- [ ] What Value Objects are needed?
- [ ] What events should be dispatched?
- [ ] What tests are required?
---
### Step 2: Create Domain Model
**Create Value Objects**:
```php
// src/Domain/Orders/ValueObjects/OrderId.php
namespace App\Domain\Orders\ValueObjects;
final readonly class OrderId
{
public function __construct(public string $value)
{
if (empty($value)) {
throw new \InvalidArgumentException('OrderId cannot be empty');
}
}
public static function generate(): self
{
return new self(\Symfony\Component\Uid\Ulid::generate());
}
public function equals(self $other): bool
{
return $this->value === $other->value;
}
}
```
**Create Domain Entity**:
```php
// src/Domain/Orders/Entities/Order.php
namespace App\Domain\Orders\Entities;
use App\Domain\Orders\ValueObjects\{OrderId, OrderStatus};
use App\Framework\Core\ValueObjects\Timestamp;
final readonly class Order
{
public function __construct(
public OrderId $id,
public UserId $userId,
public OrderStatus $status,
public Money $total,
public Timestamp $createdAt
) {}
public static function create(UserId $userId, Money $total): self
{
return new self(
id: OrderId::generate(),
userId: $userId,
status: OrderStatus::PENDING,
total: $total,
createdAt: Timestamp::now()
);
}
public function confirm(): self
{
if (!$this->status->equals(OrderStatus::PENDING)) {
throw new \DomainException('Only pending orders can be confirmed');
}
return new self(
id: $this->id,
userId: $this->userId,
status: OrderStatus::CONFIRMED,
total: $this->total,
createdAt: $this->createdAt
);
}
}
```
**Regenerate Autoloader**:
```bash
composer reload
# Or in Docker
make reload
```
---
### Step 3: Create Service Layer
**Create Service**:
```php
// src/Domain/Orders/Services/OrderService.php
namespace App\Domain\Orders\Services;
use App\Domain\Orders\{Entities\Order, Repositories\OrderRepository};
use App\Framework\Events\EventDispatcher;
use App\Domain\Orders\Events\OrderCreatedEvent;
final readonly class OrderService
{
public function __construct(
private readonly OrderRepository $repository,
private readonly EventDispatcher $events
) {}
public function createOrder(UserId $userId, Money $total): Order
{
// Create order
$order = Order::create($userId, $total);
// Persist
$this->repository->save($order);
// Dispatch event
$this->events->dispatch(new OrderCreatedEvent($order));
return $order;
}
public function confirmOrder(OrderId $orderId): Order
{
// Find order
$order = $this->repository->findById($orderId);
if ($order === null) {
throw new OrderNotFoundException($orderId);
}
// Confirm
$confirmedOrder = $order->confirm();
// Update
$this->repository->save($confirmedOrder);
// Dispatch event
$this->events->dispatch(new OrderConfirmedEvent($confirmedOrder));
return $confirmedOrder;
}
}
```
**Register Service in Container**:
```php
// src/Domain/Orders/OrdersInitializer.php
namespace App\Domain\Orders;
use App\Framework\DI\Container;
use App\Framework\Attributes\Initializer;
final readonly class OrdersInitializer
{
#[Initializer]
public function initialize(Container $container): void
{
// Register repository
$container->singleton(
OrderRepository::class,
new DatabaseOrderRepository(
$container->get(Connection::class)
)
);
// Register service
$container->singleton(
OrderService::class,
fn() => new OrderService(
$container->get(OrderRepository::class),
$container->get(EventDispatcher::class)
)
);
}
}
```
**Regenerate Autoloader**:
```bash
composer reload
```
---
### Step 4: Create API Endpoint
**Create Controller**:
```php
// src/Application/Api/OrderController.php
namespace App\Application\Api;
use App\Framework\Attributes\Route;
use App\Framework\Http\{Method, JsonResult, Request};
use App\Domain\Orders\Services\OrderService;
final readonly class OrderController
{
public function __construct(
private readonly OrderService $orderService
) {}
#[Route(path: '/api/orders', method: Method::POST)]
#[Auth(strategy: 'session')]
public function createOrder(CreateOrderRequest $request): JsonResult
{
$order = $this->orderService->createOrder(
userId: $request->userId,
total: $request->total
);
return new JsonResult([
'order_id' => $order->id->value,
'status' => $order->status->value,
'total' => $order->total->toArray()
], status: Status::CREATED);
}
#[Route(path: '/api/orders/{orderId}/confirm', method: Method::POST)]
#[Auth(strategy: 'session')]
public function confirmOrder(string $orderId): JsonResult
{
$order = $this->orderService->confirmOrder(
new OrderId($orderId)
);
return new JsonResult([
'order_id' => $order->id->value,
'status' => $order->status->value
]);
}
}
```
**Create Request Object**:
```php
// src/Application/Api/Requests/CreateOrderRequest.php
namespace App\Application\Api\Requests;
use App\Framework\Http\{Request as HttpRequest, ControllerRequest};
final readonly class CreateOrderRequest implements ControllerRequest
{
public function __construct(
public UserId $userId,
public Money $total
) {}
public static function fromHttpRequest(HttpRequest $request): self
{
$data = $request->parsedBody->toArray();
return new self(
userId: new UserId($data['user_id'] ?? ''),
total: Money::fromCents((int) ($data['total_cents'] ?? 0))
);
}
}
```
**Verify Route Registration**:
```bash
docker exec php php console.php routes:list | grep orders
```
---
### Step 5: Create Database Migration
```bash
# Generate migration
docker exec php php console.php make:migration CreateOrdersTable Domain/Orders
# Edit migration
# src/Domain/Orders/Database/Migrations/2025_01_28_120000_CreateOrdersTable.php
```
**Migration Implementation**:
```php
namespace App\Domain\Orders\Database\Migrations;
use App\Framework\Database\Migration\{Migration, SafelyReversible};
use App\Framework\Database\Schema\{Blueprint, Schema};
final readonly class CreateOrdersTable implements Migration, SafelyReversible
{
public function up(Schema $schema): void
{
$schema->create('orders', function (Blueprint $table) {
$table->string('ulid', 26)->primary();
$table->string('user_id', 26);
$table->string('status', 20);
$table->integer('total_cents');
$table->string('currency', 3)->default('EUR');
$table->timestamps();
// Indexes
$table->index('user_id');
$table->index('status');
$table->index('created_at');
// Foreign key
$table->foreign('user_id')
->references('ulid')
->on('users')
->onDelete(ForeignKeyAction::CASCADE);
});
}
public function down(Schema $schema): void
{
$schema->dropIfExists('orders');
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromString('2025_01_28_120000');
}
public function getDescription(): string
{
return 'Create orders table';
}
}
```
**Run Migration**:
```bash
docker exec php php console.php db:migrate
```
---
### Step 6: Write Tests
**Unit Test - Domain Logic**:
```php
// tests/Unit/Domain/Orders/Entities/OrderTest.php
use App\Domain\Orders\Entities\Order;
use App\Domain\Orders\ValueObjects\{OrderId, OrderStatus};
describe('Order Entity', function () {
it('creates order with pending status', function () {
$order = Order::create(
userId: new UserId('user-123'),
total: Money::fromEuros(99.99)
);
expect($order->status)->toEqual(OrderStatus::PENDING);
expect($order->total->toEuros())->toBe(99.99);
});
it('confirms pending order', function () {
$order = Order::create(
userId: new UserId('user-123'),
total: Money::fromEuros(99.99)
);
$confirmed = $order->confirm();
expect($confirmed->status)->toEqual(OrderStatus::CONFIRMED);
});
it('throws exception when confirming non-pending order', function () {
$order = Order::create(
userId: new UserId('user-123'),
total: Money::fromEuros(99.99)
)->confirm();
$order->confirm(); // Should throw
})->throws(\DomainException::class);
});
```
**Integration Test - API Endpoint**:
```php
// tests/Feature/Api/OrderControllerTest.php
use App\Application\Api\OrderController;
describe('Order API', function () {
it('creates order via API', function () {
$response = $this->post('/api/orders', [
'user_id' => 'user-123',
'total_cents' => 9999
]);
expect($response->status)->toBe(Status::CREATED);
expect($response->json('order_id'))->not->toBeNull();
expect($response->json('status'))->toBe('pending');
});
it('confirms order via API', function () {
// Create order first
$createResponse = $this->post('/api/orders', [
'user_id' => 'user-123',
'total_cents' => 9999
]);
$orderId = $createResponse->json('order_id');
// Confirm order
$confirmResponse = $this->post("/api/orders/{$orderId}/confirm");
expect($confirmResponse->status)->toBe(Status::OK);
expect($confirmResponse->json('status'))->toBe('confirmed');
});
});
```
**Run Tests**:
```bash
./vendor/bin/pest
```
---
### Step 7: Documentation
**Update API Documentation**:
```markdown
## Create Order
**Endpoint**: `POST /api/orders`
**Auth**: Session required
**Request Body**:
```json
{
"user_id": "01HQXYZ...",
"total_cents": 9999
}
```
**Response** (201 Created):
```json
{
"order_id": "01HQABC...",
"status": "pending",
"total": {
"cents": 9999,
"currency": "EUR"
}
}
```
```
---
### Step 8: Code Review Checklist
- [ ] Value Objects for all domain concepts
- [ ] Immutable entities (readonly classes)
- [ ] Events dispatched for important actions
- [ ] Exception handling implemented
- [ ] Tests cover happy path and edge cases
- [ ] Migration includes indexes and foreign keys
- [ ] API returns appropriate status codes
- [ ] Security: Authentication/Authorization applied
- [ ] Performance: N+1 queries avoided
- [ ] Documentation updated
---
## Implementing an API Endpoint
### Quick API Endpoint Guide
**1. Create Controller Method**:
```php
#[Route(path: '/api/resource', method: Method::GET)]
public function getResource(string $id): JsonResult
{
$resource = $this->service->find($id);
return new JsonResult($resource->toArray());
}
```
**2. Add Authentication**:
```php
#[Route(path: '/api/resource', method: Method::POST)]
#[Auth(strategy: 'session', roles: ['admin'])]
public function createResource(CreateRequest $request): JsonResult
{
// Only authenticated admins can access
}
```
**3. Create Request Object**:
```php
final readonly class CreateRequest implements ControllerRequest
{
public function __construct(
public string $name,
public Email $email
) {}
public static function fromHttpRequest(HttpRequest $request): self
{
$data = $request->parsedBody->toArray();
return new self(
name: trim($data['name'] ?? ''),
email: new Email($data['email'] ?? '')
);
}
}
```
**4. Handle Errors**:
```php
try {
$resource = $this->service->create($request);
return new JsonResult(
$resource->toArray(),
status: Status::CREATED
);
} catch (ValidationException $e) {
return new JsonResult(
['errors' => $e->getErrors()],
status: Status::UNPROCESSABLE_ENTITY
);
} catch (\Exception $e) {
Logger::error('[API] Unexpected error', ['exception' => $e]);
return new JsonResult(
['error' => 'Internal server error'],
status: Status::INTERNAL_SERVER_ERROR
);
}
```
**5. Test Endpoint**:
```bash
# Manual test
curl -X POST https://localhost/api/resource \
-H "Content-Type: application/json" \
-H "User-Agent: Mozilla/5.0" \
-d '{"name":"Test","email":"test@example.com"}'
# Automated test
./vendor/bin/pest --filter CreateResourceTest
```
---
### REST API Best Practices
**HTTP Methods**:
- `GET` - Retrieve resources (idempotent)
- `POST` - Create new resources
- `PUT` - Update entire resource (idempotent)
- `PATCH` - Partial update
- `DELETE` - Remove resource (idempotent)
**Status Codes**:
- `200 OK` - Successful GET/PUT/PATCH
- `201 Created` - Successful POST with resource created
- `204 No Content` - Successful DELETE
- `400 Bad Request` - Invalid request format
- `401 Unauthorized` - Authentication required
- `403 Forbidden` - Insufficient permissions
- `404 Not Found` - Resource doesn't exist
- `422 Unprocessable Entity` - Validation errors
- `429 Too Many Requests` - Rate limit exceeded
- `500 Internal Server Error` - Server-side error
**Response Format**:
```php
// Success response
return new JsonResult([
'data' => $resource->toArray(),
'meta' => [
'timestamp' => time(),
'version' => '2.0'
]
]);
// Error response
return new JsonResult([
'error' => [
'code' => 'VALIDATION_ERROR',
'message' => 'Validation failed',
'details' => [
'email' => ['Email format is invalid']
]
]
], status: Status::UNPROCESSABLE_ENTITY);
```
---
## Bug Fix Workflow
### Step 1: Reproduce the Bug
**Create Reproduction Test**:
```php
// tests/Unit/Bug123Test.php
describe('Bug #123: Orders not updating status', function () {
it('reproduces the bug', function () {
$order = Order::create(/* ... */);
$order->confirm();
// This should update status but doesn't
expect($order->status)->toBe(OrderStatus::CONFIRMED);
})->skip('Bug reproduction - currently failing');
});
```
**Run Test to Confirm Failure**:
```bash
./vendor/bin/pest --filter Bug123Test
```
---
### Step 2: Debug and Identify Root Cause
**Enable Debug Logging**:
```php
// Add logging to suspected code
Logger::debug('[OrderService] Confirming order', [
'order_id' => $order->id->value,
'current_status' => $order->status->value
]);
$confirmedOrder = $order->confirm();
Logger::debug('[OrderService] Order confirmed', [
'order_id' => $confirmedOrder->id->value,
'new_status' => $confirmedOrder->status->value
]);
```
**Check Database Queries**:
```bash
# Enable query logging
docker exec php php -r "
\$logger = new App\Framework\Database\QueryLogger();
// Run problematic code
"
```
**Use MCP Server for Analysis**:
```bash
# Analyze routes
docker exec php php console.php mcp:analyze routes
# Analyze container bindings
docker exec php php console.php mcp:analyze container
```
---
### Step 3: Fix the Bug
**Identify and Fix**:
```php
// ❌ Bug: Not saving updated order
public function confirmOrder(OrderId $orderId): Order
{
$order = $this->repository->findById($orderId);
$confirmedOrder = $order->confirm();
// Missing: $this->repository->save($confirmedOrder);
return $confirmedOrder;
}
// ✅ Fix: Save updated order
public function confirmOrder(OrderId $orderId): Order
{
$order = $this->repository->findById($orderId);
$confirmedOrder = $order->confirm();
// Save updated order
$this->repository->save($confirmedOrder);
return $confirmedOrder;
}
```
---
### Step 4: Update Tests
**Enable and Verify Test**:
```php
describe('Bug #123: Orders not updating status', function () {
it('confirms order and persists status', function () {
$order = Order::create(/* ... */);
$confirmedOrder = $this->orderService->confirmOrder($order->id);
// Verify status is updated
expect($confirmedOrder->status)->toBe(OrderStatus::CONFIRMED);
// Verify persistence
$retrieved = $this->repository->findById($order->id);
expect($retrieved->status)->toBe(OrderStatus::CONFIRMED);
});
});
```
**Run All Tests**:
```bash
./vendor/bin/pest
```
---
### Step 5: Regression Prevention
**Add Integration Test**:
```php
it('persists order status changes', function () {
$order = Order::create(/* ... */);
$this->repository->save($order);
// Confirm order
$confirmed = $this->orderService->confirmOrder($order->id);
// Retrieve from database
$fromDb = $this->repository->findById($order->id);
expect($fromDb->status)->toBe(OrderStatus::CONFIRMED);
});
```
---
### Step 6: Documentation
**Update Changelog**:
```markdown
## [2.1.1] - 2025-01-28
### Fixed
- **Orders**: Fixed bug where order status was not persisted after confirmation (#123)
- Added missing repository save call in OrderService::confirmOrder()
- Added regression test to prevent future occurrence
```
---
## Database Migration Workflow
### Creating a Migration
```bash
# Generate migration file
docker exec php php console.php make:migration AddEmailVerifiedColumn Users
# Migration file created at:
# src/Domain/Users/Database/Migrations/2025_01_28_143000_AddEmailVerifiedColumn.php
```
**Implement Migration**:
```php
namespace App\Domain\Users\Database\Migrations;
use App\Framework\Database\Migration\{Migration, SafelyReversible};
use App\Framework\Database\Schema\{Blueprint, Schema};
final readonly class AddEmailVerifiedColumn implements Migration, SafelyReversible
{
public function up(Schema $schema): void
{
$schema->table('users', function (Blueprint $table) {
// Add column
$table->boolean('email_verified')->default(false);
$table->timestamp('email_verified_at')->nullable();
// Add index for querying verified users
$table->index('email_verified');
});
}
public function down(Schema $schema): void
{
$schema->table('users', function (Blueprint $table) {
// Drop in reverse order
$table->dropIndex('email_verified');
$table->dropColumn('email_verified_at', 'email_verified');
});
}
public function getVersion(): MigrationVersion
{
return MigrationVersion::fromString('2025_01_28_143000');
}
public function getDescription(): string
{
return 'Add email verification columns to users table';
}
}
```
---
### Running Migrations
```bash
# Check migration status
docker exec php php console.php db:status
# Run pending migrations
docker exec php php console.php db:migrate
# Rollback last migration
docker exec php php console.php db:rollback
# Rollback multiple steps
docker exec php php console.php db:rollback 3
```
---
### Migration Best Practices
**Safe Rollbacks**:
- Only implement `SafelyReversible` if rollback is data-safe
- Document why a migration is not reversible if omitting `down()`
**Column Modifications**:
```php
// ✅ Safe: Adding nullable column
$table->string('new_field')->nullable();
// ⚠️ Requires data migration: Adding required column
$table->string('required_field'); // Existing rows will fail!
// ✅ Better: Add nullable, migrate data, then make required
// Migration 1: Add nullable
$table->string('required_field')->nullable();
// Migration 2 (data): Populate existing rows
// Migration 3 (schema): Make required
$table->string('required_field')->nullable(false)->change();
```
**Performance Considerations**:
```php
// ⚠️ Slow: Adding index to large table
$table->index('email'); // Blocks table on large datasets
// ✅ Better: Use online algorithm (MySQL 5.7+)
$connection->statement(
'ALTER TABLE users ADD INDEX idx_email (email) ALGORITHM=INPLACE, LOCK=NONE'
);
```
---
## Refactoring Workflow
### Safe Refactoring Process
**1. Ensure Test Coverage**:
```bash
# Run tests before refactoring
./vendor/bin/pest --coverage
# Minimum 80% coverage for critical code paths
```
**2. Make Small, Incremental Changes**:
```php
// ❌ Bad: Large refactoring in one step
// Refactor entire service layer at once
// ✅ Good: Incremental refactoring
// Step 1: Extract method
// Step 2: Move to new class
// Step 3: Update references
// Run tests after each step
```
**3. Extract Method**:
```php
// Before
public function processOrder(Order $order): void
{
// Validate payment
if ($order->total->cents < 100) {
throw new MinimumOrderException();
}
// Process payment
$this->gateway->charge($order->total);
// Send confirmation
$this->mailer->send($order->user->email, 'Order confirmed');
// Update inventory
foreach ($order->items as $item) {
$this->inventory->reduce($item->productId, $item->quantity);
}
}
// After
public function processOrder(Order $order): void
{
$this->validateOrder($order);
$this->chargePayment($order);
$this->sendConfirmation($order);
$this->updateInventory($order);
}
private function validateOrder(Order $order): void
{
if ($order->total->cents < 100) {
throw new MinimumOrderException();
}
}
private function chargePayment(Order $order): void
{
$this->gateway->charge($order->total);
}
// ... etc
```
**4. Extract Class**:
```php
// Before: God class with too many responsibilities
final readonly class OrderService
{
public function processOrder(Order $order): void { /* ... */ }
public function chargePayment(Order $order): void { /* ... */ }
public function sendConfirmation(Order $order): void { /* ... */ }
public function updateInventory(Order $order): void { /* ... */ }
}
// After: Separate concerns
final readonly class OrderService
{
public function __construct(
private readonly OrderProcessor $processor,
private readonly PaymentHandler $payment,
private readonly NotificationService $notifications,
private readonly InventoryUpdater $inventory
) {}
public function processOrder(Order $order): void
{
$this->processor->validate($order);
$this->payment->charge($order);
$this->notifications->sendConfirmation($order);
$this->inventory->update($order);
}
}
```
**5. Run Tests After Each Step**:
```bash
./vendor/bin/pest
```
---
### Refactoring Patterns
**Replace Primitive with Value Object**:
```php
// Before
final readonly class User
{
public function __construct(
public string $id,
public string $email,
public string $name
) {}
}
// After
final readonly class User
{
public function __construct(
public UserId $id,
public Email $email,
public UserName $name
) {}
}
```
**Replace Magic Numbers**:
```php
// Before
if ($user->age >= 18) {
// Can vote
}
// After
final readonly class VotingAge
{
public const MINIMUM = 18;
}
if ($user->age >= VotingAge::MINIMUM) {
// Can vote
}
```
---
## Performance Optimization Workflow
### Step 1: Measure Current Performance
```bash
# Enable performance profiling
docker exec php php console.php performance:enable
# Profile endpoint
docker exec php php console.php performance:profile /api/users
# Check slow queries
docker exec php tail -f /var/log/mysql/slow-queries.log
```
---
### Step 2: Identify Bottlenecks
**Performance Profiling**:
```php
use App\Framework\Performance\PerformanceCollector;
$collector = new PerformanceCollector();
$collector->startMeasurement('expensive_operation');
// Expensive operation
$results = $this->heavyComputation();
$collector->endMeasurement('expensive_operation');
// View results
foreach ($collector->getMeasurements() as $measurement) {
Logger::info('[Performance]', [
'operation' => $measurement->name,
'duration' => $measurement->duration->toMilliseconds()
]);
}
```
**Check N+1 Queries**:
```bash
docker exec php php console.php db:detect-n-plus-one /api/users
```
---
### Step 3: Optimize
**Cache Expensive Operations**:
```php
use App\Framework\Cache\{CacheKey, CacheItem};
use App\Framework\Core\ValueObjects\Duration;
public function getExpensiveData(): array
{
$cacheKey = CacheKey::fromString('expensive_data');
return $this->cache->remember(
$cacheKey,
fn() => $this->computeExpensiveData(),
Duration::fromMinutes(10)
);
}
```
**Fix N+1 Queries**:
```php
// ❌ Before: N+1 queries
$users = $this->userRepository->findAll();
foreach ($users as $user) {
$profile = $user->getProfile(); // Separate query each time
}
// ✅ After: Eager loading
$users = $this->userRepository->findAllWithProfiles();
// Single JOIN query
```
**Add Database Indexes**:
```php
// Create migration
$schema->table('users', function (Blueprint $table) {
// Add index for frequently queried column
$table->index('email');
// Composite index for common WHERE clauses
$table->index(['status', 'created_at']);
});
```
---
### Step 4: Verify Improvements
```bash
# Re-profile endpoint
docker exec php php console.php performance:profile /api/users
# Compare before/after metrics
docker exec php php console.php performance:compare before.json after.json
```
---
### Performance Optimization Checklist
- [ ] Identify bottleneck (profiling data)
- [ ] Set performance target (e.g., <200ms response time)
- [ ] Implement optimization
- [ ] Measure improvement
- [ ] Verify no functional regression
- [ ] Document optimization
- [ ] Monitor in production
---
## Best Practices
### General Development Guidelines
**1. Follow Framework Patterns**:
- Use Value Objects instead of primitives
- Readonly classes by default
- Final classes (no inheritance)
- Explicit dependency injection
- Event-driven architecture
**2. Security First**:
- Validate all user input
- Use parameterized queries
- Implement authentication/authorization
- Never expose sensitive data in logs
- Follow OWASP guidelines
**3. Performance Awareness**:
- Profile before optimizing
- Cache expensive operations
- Avoid N+1 queries
- Use batch operations
- Monitor production metrics
**4. Test Everything**:
- Unit tests for domain logic
- Integration tests for API endpoints
- Edge case testing
- Regression tests for bug fixes
- Performance tests for critical paths
**5. Documentation**:
- Update API documentation
- Document complex business logic
- Maintain changelog
- Code should be self-documenting
---
### Code Quality Checklist
**Before Committing**:
- [ ] Run `composer cs` (code style check)
- [ ] Run `./vendor/bin/pest` (all tests pass)
- [ ] Run `composer reload` (autoloader updated)
- [ ] Check for commented-out code
- [ ] Verify no debug statements left
- [ ] Update documentation
- [ ] Review your own changes first
**Before Merging**:
- [ ] All CI checks pass
- [ ] Code reviewed by peer
- [ ] No merge conflicts
- [ ] Migration tested
- [ ] Performance impact assessed
- [ ] Security implications considered
- [ ] Changelog updated
---
### Development Environment Setup
**Initial Setup**:
```bash
# Clone repository
git clone <repository-url>
cd michaelschiemer
# Copy environment file
cp .env.example .env
# Start Docker containers
make up
# Install dependencies
composer install
npm install
# Run migrations
docker exec php php console.php db:migrate
# Build frontend assets
npm run build
# Verify setup
curl -k https://localhost/health
```
**Daily Development**:
```bash
# Start services
make up
# Watch frontend changes
npm run dev
# Run tests
./vendor/bin/pest --watch
# Check logs
make logs
```
---
**Last Updated**: 2025-01-28
**Framework Version**: 2.x