- Move 12 markdown files from root to docs/ subdirectories - Organize documentation by category: • docs/troubleshooting/ (1 file) - Technical troubleshooting guides • docs/deployment/ (4 files) - Deployment and security documentation • docs/guides/ (3 files) - Feature-specific guides • docs/planning/ (4 files) - Planning and improvement proposals Root directory cleanup: - Reduced from 16 to 4 markdown files in root - Only essential project files remain: • CLAUDE.md (AI instructions) • README.md (Main project readme) • CLEANUP_PLAN.md (Current cleanup plan) • SRC_STRUCTURE_IMPROVEMENTS.md (Structure improvements) This improves: ✅ Documentation discoverability ✅ Logical organization by purpose ✅ Clean root directory ✅ Better maintainability
20 KiB
Naming Conventions
Framework-weite Naming Conventions für das Custom PHP Framework.
Philosophie
Kernprinzipien:
- Keine redundanten Suffixe - Domain-Code ist suffixfrei, Infrastructure hat Suffixe für Klarheit
- Semantische Klarheit - Namen beschreiben Absicht, nicht technische Implementation
- Namespace-Kontext - Namespace gibt primären Kontext, Namen sind prägnant
- Konsistenz - Einheitliche Patterns über das gesamte Framework
Übersicht
| Komponente | Pattern | Beispiel | Suffix/Prefix |
|---|---|---|---|
| View Controller | Show{Feature} |
ShowContact, ShowHome |
- |
| Action Controller | {Verb}{Feature} |
SubmitContact, CreateUser |
- |
| Repository Interface | {Entity}Repository |
UserRepository |
✅ Repository |
| Repository Implementation | {Type}{Entity}Repository |
InMemoryUserRepository |
✅ Repository |
| Service | {Domain}Service |
EmailService, UserService |
✅ Service |
| Manager | {Capability}Manager |
SessionManager, CacheManager |
✅ Manager |
| Handler (Generic) | {Capability}Handler |
ErrorHandler, FileHandler |
✅ Handler |
| Handler (Command) | {Command}Handler |
StoreContactHandler |
✅ Handler |
| CQRS Command | {Verb}{Noun} |
SendEmail, CreateUser |
- |
| Console Command | {Feature}Command |
HealthCheckCommand |
✅ Command |
| Domain Exception | {Condition} |
UserNotFound, PaymentFailed |
- |
| Technical Exception | {Type}Exception |
DatabaseException |
✅ Exception |
| Value Object | {Concept} |
Email, Price, Duration |
- |
| Event | {Entity}{Action} |
UserCreated, OrderPlaced |
- |
| Middleware | {Purpose}Middleware |
AuthMiddleware |
✅ Middleware |
| Initializer | {Service}Initializer |
DatabaseInitializer |
✅ Initializer |
Detaillierte Patterns
1. Controller Naming
Strikte Trennung zwischen View und Business Logic.
View Controllers - Show{Feature}
Zweck: Nur View-Rendering, keine Business Logic
// ✅ View-Only Controller
final readonly class ShowContact
{
#[Route('/contact', method: Method::GET)]
public function __invoke(): ViewResult
{
return new ViewResult('contact', $metaData);
}
}
final readonly class ShowHome
{
#[Route('/', method: Method::GET)]
public function __invoke(): ViewResult
{
return new ViewResult('home', $metaData);
}
}
Regeln:
- ✅ NUR GET-Requests
- ✅ NUR ViewResult als Return Type
- ✅ Darf Daten aus Repository laden (für View)
- ❌ KEINE POST/PUT/DELETE Routes
- ❌ KEINE Business Logic (CommandBus, Services)
Action Controllers - {Verb}{Feature}
Zweck: Business Logic, Form Processing, API Operations
// ✅ Action Controller mit Business Logic
final readonly class SubmitContact
{
#[Route('/contact', method: Method::POST)]
public function __invoke(ContactRequest $request): ActionResult
{
$this->commandBus->dispatch(new StoreContact(...));
return new Redirect('/contact/success');
}
}
final readonly class CreateUser
{
#[Route('/users', method: Method::POST)]
public function __invoke(CreateUserRequest $request): JsonResult
{
$user = $this->userService->create($request);
return new JsonResult($user, Status::CREATED);
}
}
Verfügbare Verben:
Submit{Feature}- Form submissionCreate{Feature}- Resource creation (POST)Update{Feature}- Resource update (PUT/PATCH)Delete{Feature}- Resource deletion (DELETE)Handle{Feature}- Generic processing (Webhooks, Events)Process{Feature}- Complex operations (Payments, Orders)Execute{Feature}- Task execution (Jobs, Commands)
Regeln:
- ✅ POST/PUT/DELETE Operations
- ✅ Business Logic (CommandBus, Services, Repositories)
- ✅ Alle Result-Typen (ViewResult, JsonResult, Redirect, etc.)
Anti-Patterns
// ❌ FALSCH: "Controller" Suffix ist redundant
class ContactController { }
class UserController { }
// ❌ FALSCH: View und Logic vermischt
class ShowContact
{
#[Route('/contact', GET)]
public function show(): ViewResult { }
#[Route('/contact', POST)] // ❌ Sollte SubmitContact sein
public function submit(): ActionResult { }
}
2. Repository Pattern
Pattern: Interface + Implementations mit Type-Prefix
// ✅ Repository Interface
interface UserRepository
{
public function find(UserId $id): ?User;
public function save(User $user): void;
}
// ✅ Repository Implementations mit Type-Prefix
final readonly class InMemoryUserRepository implements UserRepository
{
public function find(UserId $id): ?User { }
public function save(User $user): void { }
}
final readonly class DatabaseUserRepository implements UserRepository
{
public function find(UserId $id): ?User { }
public function save(User $user): void { }
}
final readonly class CacheUserRepository implements UserRepository
{
private readonly UserRepository $decorated;
public function find(UserId $id): ?User
{
return $this->cache->remember("user_{$id}",
fn() => $this->decorated->find($id)
);
}
}
Type-Prefixes:
InMemory{Entity}Repository- In-Memory ImplementationDatabase{Entity}Repository- Database-backedCache{Entity}Repository- Caching DecoratorFile{Entity}Repository- File-based Storage
Regeln:
- ✅ Interface ohne Type-Prefix
- ✅ Implementations mit Type-Prefix
- ✅ Suffix "Repository" immer vorhanden
- ❌ Kein Suffix-Pattern:
UserRepositoryDatabase
3. Service Layer
Pattern: {Domain}Service oder {Capability}Service
// ✅ Domain Services
final readonly class UserService
{
public function createUser(Email $email, UserName $name): User { }
public function findUserById(UserId $id): ?User { }
}
final readonly class EmailService
{
public function sendWelcomeEmail(User $user): void { }
public function sendPasswordResetEmail(Email $email, ResetToken $token): void { }
}
// ✅ Capability Services
final readonly class JobCleanupService
{
public function cleanupExpiredJobs(): int { }
}
final readonly class MediaCleanupService
{
public function cleanupOrphanedFiles(): void { }
}
Regeln:
- ✅ "Service" Suffix immer vorhanden
- ✅ Beschreibender Prefix (Domain oder Capability)
- ❌ Keine generischen Namen:
DataService,HelperService
4. Manager Pattern
Pattern: {Capability}Manager für komplexe State-Verwaltung
// ✅ Manager für State/Resource Management
final readonly class SessionManager
{
public function start(): void { }
public function regenerate(): void { }
public function destroy(): void { }
}
final readonly class CacheManager
{
public function clear(): void { }
public function warmup(): void { }
public function getStats(): CacheStats { }
}
final readonly class EntityManager
{
public function persist(object $entity): void { }
public function flush(): void { }
public function clear(): void { }
}
Unterschied Service vs Manager:
- Service: Business Logic, stateless Operations
- Manager: State/Resource Management, komplexe Lifecycle
5. Handler Pattern
Pattern: Namespace-basierte Unterscheidung
Log Handlers
// ✅ Log Handlers (Namespace gibt Kontext)
namespace App\Framework\Logging\Handlers;
final readonly class FileHandler implements LogHandler { }
final readonly class ConsoleHandler implements LogHandler { }
final readonly class SyslogHandler implements LogHandler { }
Command Handlers (CQRS)
// ✅ Command Handlers
namespace App\Application\Contact;
final readonly class StoreContactHandler
{
public function handle(StoreContact $command): void { }
}
namespace App\Framework\Mail\Commands;
final readonly class SendEmailCommandHandler
{
public function handle(SendEmailCommand $command): void { }
}
Regeln:
- ✅ "Handler" Suffix immer vorhanden
- ✅ Namespace gibt primären Kontext
- ✅ Kein redundanter Prefix (kein "LogFileHandler")
6. Command Pattern
Zwei Typen: CQRS Commands (kein Suffix) vs Console Commands (mit Suffix)
CQRS Commands - Business Logic
// ✅ CQRS Commands - KEIN Suffix
namespace App\Application\Contact;
final readonly class SendEmail
{
public function __construct(
public Email $to,
public string $subject,
public string $body
) {}
}
final readonly class StoreContact
{
public function __construct(
public Email $email,
public string $name,
public string $message
) {}
}
final readonly class CreateUser
{
public function __construct(
public Email $email,
public UserName $name
) {}
}
Console Commands - Infrastructure
// ✅ Console Commands - MIT "Command" Suffix
namespace App\Framework\Console\Commands;
final readonly class HealthCheckCommand implements ConsoleCommand
{
public function execute(ConsoleInput $input): int { }
}
namespace App\Framework\Database\Migration\Commands;
final readonly class MakeMigrationCommand implements ConsoleCommand
{
public function execute(ConsoleInput $input): int { }
}
Regeln:
- ✅ CQRS/Business Logic: Verb-Noun ohne Suffix
- ✅ Console/CLI: Noun + "Command" Suffix
- ❌ Keine Vermischung
7. Exception Pattern
Hybrid-Ansatz: Domain ohne Suffix, Technical mit Suffix
Domain Exceptions - OHNE Suffix
// ✅ Domain Exceptions - ausdrucksstark, kein Suffix
namespace App\Domain\User\Exceptions;
final class UserNotFound extends FrameworkException
{
public static function byId(UserId $id): self
{
return self::create(
ErrorCode::ENTITY_NOT_FOUND,
"User with ID '{$id}' not found"
)->withData(['user_id' => $id->toString()]);
}
public static function byEmail(Email $email): self
{
return self::create(
ErrorCode::ENTITY_NOT_FOUND,
"User with email not found"
)->withData(['email' => $email->getMasked()]);
}
}
final class PaymentFailed extends FrameworkException
{
public static function becauseOf(string $reason): self
{
return self::create(
ErrorCode::PAYMENT_FAILED,
"Payment failed: {$reason}"
);
}
}
Technical Exceptions - MIT Suffix
// ✅ Technical/Infrastructure Exceptions - mit Suffix für Klarheit
namespace App\Framework\Database\Exception;
final class DatabaseException extends FrameworkException { }
final class SerializationException extends FrameworkException { }
namespace App\Framework\Validation\Exceptions;
final class ValidationException extends FrameworkException { }
Regeln:
- ✅ Domain/Business Logic: KEIN "Exception" Suffix
- ✅ Framework/Infrastructure: MIT "Exception" Suffix
- ✅ Factory Methods für expressiveness:
::byId(),::becauseOf() - ✅ Base Exception behält Suffix:
FrameworkException
Verwendung:
// Sehr lesbar und ausdrucksstark
throw UserNotFound::byEmail($email);
throw PaymentFailed::becauseOf('Insufficient funds');
throw OrderCancelled::byUser($userId);
// Technical Exceptions bleiben explizit
throw new DatabaseException('Connection failed');
throw new ValidationException('Invalid input');
8. Value Object Pattern
Pattern: Keine Suffixe, beschreibende Namen
// ✅ Value Objects - kurz und prägnant
final readonly class Email
{
public function __construct(public string $value)
{
$this->validate();
}
public function getMasked(): string
{
// email → e***@example.com
}
}
final readonly class Price
{
public function __construct(
public int $cents,
public Currency $currency
) {}
public function toDecimal(): string
{
return number_format($this->cents / 100, 2);
}
}
final readonly class Duration
{
private function __construct(public int $seconds) {}
public static function fromMinutes(int $minutes): self
{
return new self($minutes * 60);
}
public static function fromHours(int $hours): self
{
return new self($hours * 3600);
}
}
Regeln:
- ✅ Beschreibende Namen ohne Suffix
- ✅ Immutable (readonly)
- ✅ Rich Domain Logic
- ❌ Kein "VO", "ValueObject" Suffix
- ❌ Keine primitiven Properties (nutze Validation)
9. Event Pattern
Pattern: {Entity}{PastTenseAction}
// ✅ Domain Events
final readonly class UserCreated
{
public function __construct(
public UserId $userId,
public Email $email,
public DateTimeImmutable $occurredAt
) {}
}
final readonly class OrderPlaced
{
public function __construct(
public OrderId $orderId,
public UserId $userId,
public Money $total,
public DateTimeImmutable $occurredAt
) {}
}
final readonly class PaymentProcessed
{
public function __construct(
public PaymentId $paymentId,
public Money $amount,
public DateTimeImmutable $occurredAt
) {}
}
Regeln:
- ✅ Past Tense (Ereignis ist bereits passiert)
- ✅ Immutable Event Data
- ✅
occurredAtTimestamp - ❌ Kein "Event" Suffix (Namespace gibt Kontext)
10. Middleware Pattern
Pattern: {Purpose}Middleware
// ✅ Middleware
final readonly class AuthMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response { }
}
final readonly class CsrfMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response { }
}
final readonly class LoggingMiddleware implements Middleware
{
public function process(Request $request, callable $next): Response { }
}
Regeln:
- ✅ "Middleware" Suffix immer vorhanden
- ✅ Beschreibender Purpose-Prefix
11. Initializer Pattern
Pattern: {Service}Initializer für DI Container Setup
// ✅ Initializers
final readonly class DatabaseInitializer
{
#[Initializer]
public function __invoke(Container $container): ConnectionInterface
{
return new PdoConnection(
DatabaseConfig::fromEnvironment($this->env)
);
}
}
final readonly class LoggerInitializer
{
#[Initializer]
public function __invoke(Container $container): Logger
{
return new DefaultLogger(
handlers: [$this->fileHandler, $this->consoleHandler]
);
}
}
Regeln:
- ✅ "Initializer" Suffix
- ✅
#[Initializer]Attribute - ✅ Returns configured service instance
Directory Structure
Feature-basierte, flache Struktur (DDD-Style)
src/
├── Application/
│ ├── Contact/
│ │ ├── ShowContact.php # View Controller
│ │ ├── SubmitContact.php # Action Controller
│ │ ├── ContactRequest.php # Request VO
│ │ ├── StoreContact.php # CQRS Command
│ │ └── StoreContactHandler.php # Command Handler
│ ├── User/
│ │ ├── ShowUserProfile.php
│ │ ├── UpdateUserProfile.php
│ │ ├── DeleteUser.php
│ │ └── UserService.php
│ └── Admin/
│ ├── ShowImageManager.php
│ ├── CreateImage.php
│ └── DeleteImage.php
├── Domain/
│ ├── User/
│ │ ├── User.php # Entity
│ │ ├── UserId.php # Value Object
│ │ ├── UserRepository.php # Interface
│ │ ├── Exceptions/
│ │ │ ├── UserNotFound.php # Domain Exception
│ │ │ └── InvalidEmail.php
│ │ └── Events/
│ │ ├── UserCreated.php
│ │ └── UserDeleted.php
│ └── Order/
│ ├── Order.php
│ ├── OrderId.php
│ ├── OrderRepository.php
│ └── Exceptions/
│ └── OrderNotFound.php
└── Framework/
├── Database/
│ ├── DatabaseManager.php
│ ├── EntityManager.php
│ ├── Exception/
│ │ └── DatabaseException.php # Technical Exception
│ └── Repository/
│ └── EntityRepository.php
└── Logging/
├── Logger.php
├── LoggerInitializer.php
└── Handlers/
├── FileHandler.php
├── ConsoleHandler.php
└── SyslogHandler.php
Regeln:
- ✅ Feature-basierte Gruppierung
- ✅ Zusammengehörige Klassen im selben Namespace
- ✅ Keine
/Controllers/Subfolder - ✅ Exceptions in
/Exceptions/Subnamespace - ✅ Events in
/Events/Subnamespace
Method Naming
Public API Methods
// ✅ Verb-Noun Pattern
public function getUser(UserId $id): User { }
public function createOrder(CreateOrderData $data): Order { }
public function updateProfile(UpdateProfileData $data): void { }
public function deleteImage(ImageId $id): void { }
// ✅ Boolean Methods
public function isValid(): bool { }
public function hasExpired(): bool { }
public function canAccess(User $user): bool { }
public function shouldRetry(): bool { }
// ✅ Factory Methods
public static function fromArray(array $data): self { }
public static function fromString(string $value): self { }
public static function create(...$args): self { }
// ✅ Transformation Methods
public function toArray(): array { }
public function toString(): string { }
public function toJson(): string { }
public function toDecimal(): string { }
Readonly Classes - Public Properties statt Getters
// ✅ Readonly Classes: Public Properties
final readonly class User
{
public function __construct(
public UserId $id,
public Email $email,
public UserName $name
) {}
}
// Verwendung:
$user->email; // ✅ Direkt zugreifen
$user->name; // ✅ Kein Getter nötig
// ❌ FALSCH bei readonly Classes
public function getEmail(): Email { return $this->email; } // Redundant!
Anti-Patterns
❌ Vermeiden
// ❌ Redundante Suffixe bei Domain Code
class UserController { } // Nutze: ShowUser, CreateUser
class EmailVO { } // Nutze: Email
class SendEmailCommand { } // Nutze: SendEmail (CQRS)
// ❌ Generische Namen
class Helper { }
class Util { }
class Manager { } // Ohne Kontext
class Service { } // Ohne Kontext
// ❌ Technische Details in Namen
class MySQLUserRepository { } // Nutze: DatabaseUserRepository
class RedisCache { } // Nutze: CacheManager mit Redis-Implementation
// ❌ Unklare Abkürzungen
class UsrCtrl { }
class PrdMgr { }
// ❌ Vermischte Patterns
class ShowContact
{
public function show(): ViewResult { }
public function submit(): ActionResult { } // Sollte SubmitContact sein
}
Zusammenfassung
Domain Code (Business Logic):
- Keine redundanten Suffixe
- Ausdrucksstark und semantisch
- Beispiele:
ShowContact,SubmitContact,UserNotFound,Email
Infrastructure Code (Framework/Technical):
- Explizite Suffixe für Klarheit
- Beispiele:
DatabaseException,HealthCheckCommand,FileHandler,UserRepository
Hybride Komponenten:
- Nutze Namespace für primären Kontext
- Suffix nur wenn wirklich nötig
- Beispiele:
Handler(verschiedene Typen),Service(immer mit Suffix)
Diese Conventions schaffen Konsistenz über das gesamte Framework und machen den Code selbstdokumentierend und wartbar.