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

28 KiB

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:

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

// 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:

// 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:

composer reload
# Or in Docker
make reload

Step 3: Create Service Layer

Create Service:

// 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:

// 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:

composer reload

Step 4: Create API Endpoint

Create Controller:

// 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:

// 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:

docker exec php php console.php routes:list | grep orders

Step 5: Create Database Migration

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

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:

docker exec php php console.php db:migrate

Step 6: Write Tests

Unit Test - Domain Logic:

// 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:

// 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:

./vendor/bin/pest

Step 7: Documentation

Update API Documentation:

## Create Order

**Endpoint**: `POST /api/orders`
**Auth**: Session required

**Request Body**:
```json
{
  "user_id": "01HQXYZ...",
  "total_cents": 9999
}

Response (201 Created):

{
  "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:

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

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:

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:

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

// 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:

// 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:

./vendor/bin/pest --filter Bug123Test

Step 2: Debug and Identify Root Cause

Enable Debug Logging:

// 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:

# Enable query logging
docker exec php php -r "
\$logger = new App\Framework\Database\QueryLogger();
// Run problematic code
"

Use MCP Server for Analysis:

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

// ❌ 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:

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:

./vendor/bin/pest

Step 5: Regression Prevention

Add Integration Test:

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:

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

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

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

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

// ✅ 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:

// ⚠️ 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:

# Run tests before refactoring
./vendor/bin/pest --coverage

# Minimum 80% coverage for critical code paths

2. Make Small, Incremental Changes:

// ❌ 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:

// 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:

// 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:

./vendor/bin/pest

Refactoring Patterns

Replace Primitive with Value Object:

// 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:

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

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

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:

docker exec php php console.php db:detect-n-plus-one /api/users

Step 3: Optimize

Cache Expensive Operations:

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:

// ❌ 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:

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

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

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

# 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