# 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 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