1260 lines
28 KiB
Markdown
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
|