From 74129bdb0891bfdf65e822ad25c51a3c25f66d65 Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Tue, 14 Oct 2025 22:25:56 +0200 Subject: [PATCH 1/3] :technologist: Improvement of best practices in boilerplates --- src/boilerplate/fastapi.rs | 81 ++- src/boilerplate/flask.rs | 4 + src/boilerplate/go.rs | 13 + src/boilerplate/templates.rs | 956 ++++++++++++++++++++++++++++++----- 4 files changed, 925 insertions(+), 129 deletions(-) diff --git a/src/boilerplate/fastapi.rs b/src/boilerplate/fastapi.rs index 92ca061..d85ff58 100644 --- a/src/boilerplate/fastapi.rs +++ b/src/boilerplate/fastapi.rs @@ -90,6 +90,14 @@ impl FastAPIGenerator { let security_content = replace_template_vars_string(SECURITY_PY, &vars); write_file(base_path.join("app/core/security.py"), &security_content)?; + // Structured logging module (new in 2025) + let logging_content = replace_template_vars_string(crate::boilerplate::templates::LOGGING_PY, &vars); + write_file(base_path.join("app/core/logging.py"), &logging_content)?; + + // Rate limiting module (new in 2025) + let rate_limiting_content = replace_template_vars_string(crate::boilerplate::templates::RATE_LIMITING_PY, &vars); + write_file(base_path.join("app/core/rate_limiting.py"), &rate_limiting_content)?; + Ok(()) } @@ -100,14 +108,79 @@ impl FastAPIGenerator { write_file(base_path.join("app/api/__init__.py"), "")?; write_file(base_path.join("app/api/v1/__init__.py"), "")?; - // Health endpoint (at root level) - let health_py = r#"from fastapi import APIRouter + // Enhanced health endpoint with readiness and liveness (2025 best practices) + let health_py = r#"from __future__ import annotations + +from typing import Dict, Any +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession +import structlog +import time +from datetime import datetime, timezone + +from app.core.config import settings +from app.database.connection import get_async_session router = APIRouter() +logger = structlog.get_logger() + +# Store start time for uptime calculation +start_time = time.time() + +# Health check dependencies +async def check_database(db: AsyncSession = Depends(get_async_session)) -> bool: + """Check database connectivity""" + try: + await db.execute("SELECT 1") + return True + except Exception as e: + logger.error("Database health check failed", error=str(e)) + return False @router.get("/health") -async def health_check(): - return {"status": "healthy", "service": "{{project_name}} API"} +async def health_check() -> Dict[str, Any]: + """Basic health check endpoint""" + return { + "status": "healthy", + "service": "{{project_name}} API", + "version": "1.0.0", + "timestamp": datetime.now(timezone.utc).isoformat(), + "environment": settings.ENVIRONMENT + } + +@router.get("/health/ready") +async def readiness_check(db_healthy: bool = Depends(check_database)) -> Dict[str, Any]: + """Readiness check - can the service handle requests?""" + checks = { + "database": db_healthy, + "service": True # Add more checks as needed + } + + all_healthy = all(checks.values()) + + if not all_healthy: + logger.warning("Readiness check failed", checks=checks) + raise HTTPException( + status_code=status.HTTP_503_SERVICE_UNAVAILABLE, + detail={"status": "not ready", "checks": checks} + ) + + return { + "status": "ready", + "service": "{{project_name}} API", + "checks": checks, + "timestamp": datetime.now(timezone.utc).isoformat() + } + +@router.get("/health/live") +async def liveness_check() -> Dict[str, Any]: + """Liveness check - is the service alive?""" + return { + "status": "alive", + "service": "{{project_name}} API", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": time.time() - start_time + } "#; let health_content = replace_template_vars_string(health_py, &vars); write_file(base_path.join("app/api/health.py"), &health_content)?; diff --git a/src/boilerplate/flask.rs b/src/boilerplate/flask.rs index 6098712..c750a87 100644 --- a/src/boilerplate/flask.rs +++ b/src/boilerplate/flask.rs @@ -69,6 +69,10 @@ impl FlaskGenerator { write_file(base_path.join("app/core/config.py"), &config_content)?; write_file(base_path.join("app/core/__init__.py"), "")?; + // Structured logging module (new in 2025) + let logging_content = replace_template_vars_string(crate::boilerplate::templates::flask::FLASK_LOGGING_PY, &vars); + write_file(base_path.join("app/core/logging.py"), &logging_content)?; + // Extensions (Flask extensions initialization) let extensions_content = replace_template_vars_string(EXTENSIONS_PY, &vars); write_file(base_path.join("app/core/extensions.py"), &extensions_content)?; diff --git a/src/boilerplate/go.rs b/src/boilerplate/go.rs index 68f4486..9cc7ca2 100644 --- a/src/boilerplate/go.rs +++ b/src/boilerplate/go.rs @@ -332,6 +332,15 @@ func runMigrations(db *sqlx.DB) error { Ok(()) } + fn generate_logger_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + let logger_content = replace_template_vars_string(crate::boilerplate::templates::go::GO_LOGGER, &vars); + write_file(base_path.join("pkg/logger/logger.go"), &logger_content)?; + + Ok(()) + } + fn generate_auth_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { let vars = self.get_template_vars(config); @@ -1636,6 +1645,10 @@ impl BoilerplateGenerator for GoGenerator { println!(" 💾 Setting up database integration..."); self.generate_database_files(config, base_path)?; + // Generate logger module with slog (new in 2025) + println!(" 📋 Setting up structured logging..."); + self.generate_logger_files(config, base_path)?; + // Generate auth files println!(" 🔐 Creating authentication system..."); self.generate_auth_files(config, base_path)?; diff --git a/src/boilerplate/templates.rs b/src/boilerplate/templates.rs index 359d3a4..97a58c8 100644 --- a/src/boilerplate/templates.rs +++ b/src/boilerplate/templates.rs @@ -1,57 +1,91 @@ //! Template definitions for FastAPI, Flask, and Go boilerplates pub mod fastapi { - pub const MAIN_PY: &str = r#"from fastapi import FastAPI, APIRouter, HTTPException, Depends, status -from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + pub const MAIN_PY: &str = r#"from __future__ import annotations + +from typing import Any +from fastapi import FastAPI, APIRouter, Request, Response, status from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse import uvicorn -import logging -import os +import structlog +import uuid from contextlib import asynccontextmanager from app.core.config import settings +from app.core.logging import setup_logging, RequestIdMiddleware from app.core.security import verify_token from app.database.connection import init_database, close_database_connection from app.api import health from app.api.v1 import auth, users +from app.core.rate_limiting import RateLimitMiddleware -# Configure logging -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger(__name__) - -security = HTTPBearer() +# Setup structured logging +setup_logging() +logger = structlog.get_logger() @asynccontextmanager async def lifespan(app: FastAPI): # Startup - logger.info("Starting up {{project_name}} application...") + logger.info("Starting up {{project_name}} application", service="{{project_name}}", version="1.0.0") await init_database() yield # Shutdown - logger.info("Shutting down {{project_name}} application...") + logger.info("Shutting down {{project_name}} application", service="{{project_name}}") await close_database_connection() app = FastAPI( title="{{project_name}} API", - description="Production-ready {{project_name}} API with authentication", + description="Production-ready {{project_name}} API with authentication and observability", version="1.0.0", - lifespan=lifespan + lifespan=lifespan, + openapi_tags=[ + {"name": "health", "description": "Health and readiness checks"}, + {"name": "authentication", "description": "User authentication operations"}, + {"name": "users", "description": "User management operations"}, + ], ) +# Add request ID middleware first +app.add_middleware(RequestIdMiddleware) + +# Rate limiting middleware +app.add_middleware(RateLimitMiddleware, calls=100, period=60) + # Security middleware -app.add_middleware(TrustedHostMiddleware, allowed_hosts=["localhost", "127.0.0.1"]) +app.add_middleware(TrustedHostMiddleware, allowed_hosts=settings.ALLOWED_HOSTS + ["localhost", "127.0.0.1"]) app.add_middleware( CORSMiddleware, allow_origins=settings.ALLOWED_HOSTS, allow_credentials=True, - allow_methods=["GET", "POST", "PUT", "DELETE"], + allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allow_headers=["*"], + expose_headers=["X-Request-ID"], ) +# Exception handler for better error responses +@app.exception_handler(Exception) +async def global_exception_handler(request: Request, exc: Exception) -> JSONResponse: + request_id = getattr(request.state, "request_id", str(uuid.uuid4())) + logger.error( + "Unhandled exception", + request_id=request_id, + path=request.url.path, + method=request.method, + error=str(exc), + exc_info=True + ) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": "Internal server error", + "request_id": request_id, + "message": "An unexpected error occurred" + }, + headers={"X-Request-ID": request_id} + ) + # Include routers app.include_router(health.router, tags=["health"]) @@ -62,20 +96,31 @@ api_v1.include_router(users.router, prefix="/users", tags=["users"]) app.include_router(api_v1) @app.get("/") -async def root(): - return {"message": "{{project_name}} API is running", "version": "1.0.0"} +async def root(request: Request) -> dict[str, Any]: + request_id = getattr(request.state, "request_id", str(uuid.uuid4())) + logger.info("Root endpoint accessed", request_id=request_id) + return { + "message": "{{project_name}} API is running", + "version": "1.0.0", + "environment": settings.ENVIRONMENT, + "request_id": request_id + } if __name__ == "__main__": uvicorn.run( "app.main:app", host="0.0.0.0", port=8000, - reload=True if settings.ENVIRONMENT == "development" else False + reload=settings.ENVIRONMENT == "development", + access_log=False, # Use structured logging instead + log_config=None # Disable default logging ) "#; - pub const CONFIG_PY: &str = r#"from pydantic_settings import BaseSettings -from pydantic import ConfigDict + pub const CONFIG_PY: &str = r#"from __future__ import annotations + +from pydantic_settings import BaseSettings +from pydantic import ConfigDict, Field, validator from typing import List import os @@ -83,30 +128,83 @@ class Settings(BaseSettings): # Application PROJECT_NAME: str = "{{project_name}}" VERSION: str = "1.0.0" - ENVIRONMENT: str = "development" + ENVIRONMENT: str = Field(default="development", description="Environment: development, staging, production") + DEBUG: bool = Field(default=False, description="Enable debug mode") # Security - SECRET_KEY: str = "{{secret_key}}" - ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 - REFRESH_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days + SECRET_KEY: str = Field("{{secret_key}}", min_length=32, description="Secret key for JWT signing") + ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, ge=5, le=1440) + REFRESH_TOKEN_EXPIRE_MINUTES: int = Field(default=60 * 24 * 7, ge=60) # 7 days ALGORITHM: str = "HS256" - # CORS - ALLOWED_HOSTS: List[str] = ["http://localhost:3000", "http://127.0.0.1:3000"] + # CORS and Security + ALLOWED_HOSTS: List[str] = Field( + default=["http://localhost:3000", "http://127.0.0.1:3000"], + description="Allowed CORS origins" + ) + TRUSTED_HOSTS: List[str] = Field( + default=["localhost", "127.0.0.1"], + description="Trusted hosts for TrustedHostMiddleware" + ) # Database MONGODB_URL: str = "mongodb://localhost:27017" DATABASE_NAME: str = "{{snake_case}}_db" - DATABASE_URL: str = "postgresql://user:password@localhost/{{snake_case}}_db" + DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost/{{snake_case}}_db" POSTGRES_PASSWORD: str = "changeme" + # Connection Pool Settings + DB_POOL_SIZE: int = Field(default=20, ge=5, le=100) + DB_POOL_OVERFLOW: int = Field(default=0, ge=0, le=50) + # Redis (for caching/sessions) REDIS_URL: str = "redis://localhost:6379" + # Logging + LOG_LEVEL: str = Field(default="INFO", description="Logging level") + LOG_FORMAT: str = Field(default="json", description="Logging format: json or text") + + # Rate Limiting + RATE_LIMIT_ENABLED: bool = Field(default=True, description="Enable rate limiting") + RATE_LIMIT_CALLS: int = Field(default=100, ge=1, description="Rate limit calls per period") + RATE_LIMIT_PERIOD: int = Field(default=60, ge=1, description="Rate limit period in seconds") + + # OpenTelemetry (optional) + OTEL_ENABLED: bool = Field(default=False, description="Enable OpenTelemetry") + OTEL_ENDPOINT: str = Field(default="", description="OpenTelemetry endpoint") + + @validator("ENVIRONMENT") + def validate_environment(cls, v: str) -> str: + if v not in ["development", "staging", "production"]: + raise ValueError("ENVIRONMENT must be development, staging, or production") + return v + + @validator("LOG_LEVEL") + def validate_log_level(cls, v: str) -> str: + if v.upper() not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: + raise ValueError("LOG_LEVEL must be DEBUG, INFO, WARNING, ERROR, or CRITICAL") + return v.upper() + + @validator("LOG_FORMAT") + def validate_log_format(cls, v: str) -> str: + if v not in ["json", "text"]: + raise ValueError("LOG_FORMAT must be json or text") + return v + model_config = ConfigDict( env_file=".env", - case_sensitive=True + case_sensitive=True, + validate_assignment=True, + extra="forbid" ) + + @property + def is_development(self) -> bool: + return self.ENVIRONMENT == "development" + + @property + def is_production(self) -> bool: + return self.ENVIRONMENT == "production" settings = Settings() "#; @@ -173,26 +271,58 @@ def generate_secure_secret() -> str: return secrets.token_urlsafe(32) "#; - pub const REQUIREMENTS_TXT: &str = r#"fastapi==0.104.1 -uvicorn[standard]==0.24.0 -gunicorn==21.2.0 -pydantic==2.5.0 -pydantic-settings==2.1.0 + pub const REQUIREMENTS_TXT: &str = r#"# Core Framework - Latest 2025 versions +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +gunicorn==23.0.0 +pydantic==2.9.0 +pydantic-settings==2.5.0 + +# Authentication & Security python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 -python-multipart==0.0.6{{#if mongodb}} -motor==3.3.2 -pymongo==4.6.0{{/if}}{{#if postgresql}} +python-multipart==0.0.12 + +# Structured Logging +structlog==24.4.0 +python-json-logger==2.0.7 + +# Database{{#if mongodb}} +motor==3.6.0 +pymongo==4.10.1{{/if}}{{#if postgresql}} asyncpg==0.29.0 -sqlalchemy[asyncio]==2.0.23 -alembic==1.13.1{{/if}} -redis==5.0.1 -pytest==7.4.3 -pytest-asyncio==0.21.1 -httpx==0.25.2 +sqlalchemy[asyncio]==2.0.36 +sqlmodel==0.0.22 +alembic==1.14.0{{/if}} + +# Caching & Storage +redis==5.1.1 + +# Performance & Monitoring +slowapi==0.1.9 +prometheus-fastapi-instrumentator==7.0.0 + +# Optional: OpenTelemetry +opentelemetry-api==1.27.0 +opentelemetry-sdk==1.27.0 +opentelemetry-instrumentation-fastapi==0.48b0 +opentelemetry-instrumentation-sqlalchemy==0.48b0 +opentelemetry-exporter-otlp==1.27.0 + +# Development & Testing +pytest==8.3.3 +pytest-asyncio==0.24.0 +pytest-cov==5.0.0 +httpx==0.27.2 +factory-boy==3.3.1 + +# Code Quality +ruff==0.7.4 +mypy==1.13.0 +pre-commit==4.0.1 "#; - pub const DOCKERFILE: &str = r#"FROM python:3.11-slim AS builder + pub const DOCKERFILE: &str = r#"FROM python:3.12-slim AS builder # Install build dependencies RUN apt-get update && apt-get install -y --no-install-recommends gcc \ @@ -443,12 +573,242 @@ server { "#; } + // New structured logging module + pub const LOGGING_PY: &str = r#"from __future__ import annotations + +import logging +import logging.config +import structlog +import uuid +from typing import Any, Dict +from fastapi import Request, Response +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request as StarletteRequest + +from app.core.config import settings + +def setup_logging() -> None: + """Configure structured logging with structlog""" + + # Configure structlog + if settings.LOG_FORMAT == "json": + processors = [ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="iso"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + structlog.processors.JSONRenderer() + ] + else: + processors = [ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S"), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.dev.ConsoleRenderer() + ] + + structlog.configure( + processors=processors, + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + # Configure standard library logging + logging.basicConfig( + format="%(message)s", + level=getattr(logging, settings.LOG_LEVEL), + force=True, + ) + +class RequestIdMiddleware(BaseHTTPMiddleware): + """Middleware to add request ID to each request""" + + async def dispatch(self, request: Request, call_next) -> Response: + # Generate or extract request ID + request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) + + # Store request ID in request state + request.state.request_id = request_id + + # Set up structured logging context + logger = structlog.get_logger() + logger = logger.bind( + request_id=request_id, + method=request.method, + path=request.url.path, + user_agent=request.headers.get("User-Agent", ""), + remote_addr=request.client.host if request.client else None + ) + + # Log request + logger.info( + "Request started", + query_params=dict(request.query_params) + ) + + try: + response = await call_next(request) + + # Log response + logger.info( + "Request completed", + status_code=response.status_code, + response_time_ms=None # Could add timing here + ) + + # Add request ID to response headers + response.headers["X-Request-ID"] = request_id + + return response + + except Exception as exc: + logger.error( + "Request failed", + error=str(exc), + exc_info=True + ) + raise +"#; + + // Rate limiting middleware + pub const RATE_LIMITING_PY: &str = r#"from __future__ import annotations + +import time +from typing import Optional +from fastapi import Request, HTTPException, status +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import Response +import structlog +from collections import defaultdict, deque +from threading import Lock + +from app.core.config import settings + +logger = structlog.get_logger() + +class InMemoryRateLimiter: + """Simple in-memory rate limiter using sliding window""" + + def __init__(self): + self.requests = defaultdict(deque) + self.lock = Lock() + + def is_allowed(self, key: str, limit: int, window: int) -> bool: + """Check if request is allowed under rate limit""" + now = time.time() + + with self.lock: + # Remove old requests outside the window + while self.requests[key] and self.requests[key][0] <= now - window: + self.requests[key].popleft() + + # Check if under limit + if len(self.requests[key]) >= limit: + return False + + # Add current request + self.requests[key].append(now) + return True + + def get_remaining(self, key: str, limit: int, window: int) -> int: + """Get remaining requests allowed""" + now = time.time() + + with self.lock: + # Clean old requests + while self.requests[key] and self.requests[key][0] <= now - window: + self.requests[key].popleft() + + return max(0, limit - len(self.requests[key])) + + def get_reset_time(self, key: str, window: int) -> Optional[float]: + """Get time when rate limit resets""" + with self.lock: + if not self.requests[key]: + return None + return self.requests[key][0] + window + +class RateLimitMiddleware(BaseHTTPMiddleware): + """Rate limiting middleware""" + + def __init__(self, app, calls: int = 100, period: int = 60): + super().__init__(app) + self.calls = calls + self.period = period + self.limiter = InMemoryRateLimiter() + + def get_client_key(self, request: Request) -> str: + """Get client identifier for rate limiting""" + # Use X-Forwarded-For if available (behind proxy) + forwarded_for = request.headers.get("X-Forwarded-For") + if forwarded_for: + return forwarded_for.split(",")[0].strip() + + # Fall back to direct client IP + return request.client.host if request.client else "unknown" + + async def dispatch(self, request: Request, call_next) -> Response: + if not settings.RATE_LIMIT_ENABLED: + return await call_next(request) + + # Skip rate limiting for health checks + if request.url.path in ["/health", "/health/ready", "/health/live"]: + return await call_next(request) + + client_key = self.get_client_key(request) + + # Check rate limit + if not self.limiter.is_allowed(client_key, self.calls, self.period): + logger.warning( + "Rate limit exceeded", + client=client_key, + path=request.url.path, + method=request.method + ) + + reset_time = self.limiter.get_reset_time(client_key, self.period) + + raise HTTPException( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + detail="Rate limit exceeded", + headers={ + "X-RateLimit-Limit": str(self.calls), + "X-RateLimit-Remaining": "0", + "X-RateLimit-Reset": str(int(reset_time)) if reset_time else "", + "Retry-After": str(self.period) + } + ) + + # Add rate limit headers to response + response = await call_next(request) + + remaining = self.limiter.get_remaining(client_key, self.calls, self.period) + reset_time = self.limiter.get_reset_time(client_key, self.period) + + response.headers["X-RateLimit-Limit"] = str(self.calls) + response.headers["X-RateLimit-Remaining"] = str(remaining) + if reset_time: + response.headers["X-RateLimit-Reset"] = str(int(reset_time)) + + return response +"#; + pub mod go { pub const MAIN_GO: &str = r#"package main import ( "context" - "log" + "log/slog" "net/http" "os" "os/signal" @@ -460,91 +820,125 @@ import ( "{{module_name}}/internal/handlers" "{{module_name}}/internal/middleware" "{{module_name}}/internal/routes" + "{{module_name}}/pkg/logger" "github.com/gin-gonic/gin" "github.com/joho/godotenv" ) func main() { + // Setup structured logging first + logger.Setup() + log := slog.Default() + // Load environment variables if err := godotenv.Load(); err != nil { - log.Println("No .env file found") + log.Warn("No .env file found", "error", err) } // Load configuration cfg := config.Load() - // Set gin mode + log.Info("Starting {{project_name}} application", + "environment", cfg.Environment, + "port", cfg.Port, + "version", "1.0.0") + + // Set gin mode and disable default logging (we use slog) if cfg.Environment == "production" { gin.SetMode(gin.ReleaseMode) } + gin.DisableConsoleColor() - // Initialize database - db, err := database.Connect(cfg) + // Initialize database with context + ctx := context.Background() + db, err := database.Connect(ctx, cfg) if err != nil { - log.Fatal("Failed to connect to database:", err) + log.Error("Failed to connect to database", "error", err) + os.Exit(1) } defer database.Close(db) // Initialize handlers h := handlers.New(db, cfg) - // Setup router + // Setup router with structured logging middleware router := gin.New() - router.Use(gin.Logger()) - router.Use(gin.Recovery()) + router.Use(middleware.StructuredLogger()) + router.Use(middleware.Recovery()) router.Use(middleware.CORS()) router.Use(middleware.Security()) + router.Use(middleware.RequestID()) // Setup routes routes.Setup(router, h) - // Create server + // Create server with improved settings srv := &http.Server{ - Addr: ":" + cfg.Port, - Handler: router, + Addr: ":" + cfg.Port, + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 60 * time.Second, + MaxHeaderBytes: 1 << 20, // 1MB } // Start server in goroutine go func() { - log.Printf("Server starting on port %s", cfg.Port) + log.Info("Server starting", "port", cfg.Port, "environment", cfg.Environment) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Fatalf("Server failed to start: %v", err) + log.Error("Server failed to start", "error", err) + os.Exit(1) } }() - // Wait for interrupt signal + // Wait for interrupt signal with graceful shutdown quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit - log.Println("Shutting down server...") + log.Info("Shutting down server gracefully...") - // Graceful shutdown - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + // Graceful shutdown with extended timeout + ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server forced to shutdown:", err) + log.Error("Server forced shutdown", "error", err) + os.Exit(1) } - log.Println("Server exiting") + log.Info("Server shutdown completed successfully") } "#; pub const GO_MOD: &str = r#"module {{module_name}} -go 1.21 +go 1.22 require ( - github.com/gin-gonic/gin v1.9.1 - github.com/golang-jwt/jwt/v5 v5.0.0 - github.com/joho/godotenv v1.4.0 - golang.org/x/crypto v0.15.0 - go.mongodb.org/mongo-driver v1.12.1 + // Core framework + github.com/gin-gonic/gin v1.10.0 + + // Authentication & Security + github.com/golang-jwt/jwt/v5 v5.2.1 + golang.org/x/crypto v0.28.0 + + // Configuration + github.com/joho/godotenv v1.5.1 + + // Database drivers & ORM + go.mongodb.org/mongo-driver v1.17.1 github.com/lib/pq v1.10.9 - github.com/jmoiron/sqlx v1.3.5 - github.com/google/uuid v1.4.0 - github.com/stretchr/testify v1.8.4 + github.com/jmoiron/sqlx v1.4.0 + + // Utilities + github.com/google/uuid v1.6.0 + + // Validation + github.com/go-playground/validator/v10 v10.22.1 + + // Testing + github.com/stretchr/testify v1.9.0 ) "#; @@ -591,29 +985,124 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ # Run the application CMD ["./main"] +"#; + + // New structured logging module for Go with slog + pub const GO_LOGGER: &str = r#"package logger + +import ( + "context" + "log/slog" + "os" + "strings" +) + +// LogFormat represents the logging format +type LogFormat string + +const ( + FormatJSON LogFormat = "json" + FormatText LogFormat = "text" +) + +// Setup initializes structured logging with slog +func Setup() { + logLevel := getLogLevel() + logFormat := getLogFormat() + + var handler slog.Handler + + switch logFormat { + case FormatJSON: + handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: logLevel, + AddSource: true, + }) + default: + handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: logLevel, + AddSource: true, + }) + } + + logger := slog.New(handler) + slog.SetDefault(logger) +} + +// getLogLevel returns the log level from environment +func getLogLevel() slog.Level { + level := strings.ToUpper(os.Getenv("LOG_LEVEL")) + switch level { + case "DEBUG": + return slog.LevelDebug + case "INFO": + return slog.LevelInfo + case "WARN", "WARNING": + return slog.LevelWarn + case "ERROR": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +// getLogFormat returns the log format from environment +func getLogFormat() LogFormat { + format := strings.ToLower(os.Getenv("LOG_FORMAT")) + switch format { + case "json": + return FormatJSON + default: + return FormatText + } +} + +// WithRequestID adds request ID to the logger context +func WithRequestID(ctx context.Context, requestID string) context.Context { + logger := slog.Default().With("request_id", requestID) + return context.WithValue(ctx, "logger", logger) +} + +// FromContext returns a logger with context information +func FromContext(ctx context.Context) *slog.Logger { + if logger, ok := ctx.Value("logger").(*slog.Logger); ok { + return logger + } + return slog.Default() +} "#; } pub mod flask { - pub const APP_INIT_PY: &str = r#"from flask import Flask + pub const APP_INIT_PY: &str = r#"from __future__ import annotations + +from typing import Type +from flask import Flask, request, g from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate from flask_jwt_extended import JWTManager from flask_cors import CORS from flask_limiter import Limiter from flask_limiter.util import get_remote_address -import logging +import structlog +import uuid +import time import os from app.core.config import Config from app.core.extensions import db, migrate, jwt, cors, limiter +from app.core.logging import setup_logging from app.api import health from app.api.v1 import auth, users -def create_app(config_class=Config): +def create_app(config_class: Type[Config] = Config) -> Flask: app = Flask(__name__) app.config.from_object(config_class) + # Setup structured logging first + setup_logging(app) + logger = structlog.get_logger() + # Initialize extensions db.init_app(app) migrate.init_app(app, db) @@ -621,18 +1110,54 @@ def create_app(config_class=Config): cors.init_app(app) limiter.init_app(app) - # Configure logging - if not app.debug and not app.testing: - if not os.path.exists('logs'): - os.mkdir('logs') + # Request ID middleware + @app.before_request + def before_request() -> None: + g.request_id = request.headers.get('X-Request-ID', str(uuid.uuid4())) + g.start_time = time.time() - file_handler = logging.FileHandler('logs/{{snake_case}}.log') - file_handler.setFormatter(logging.Formatter( - '%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]')) - file_handler.setLevel(logging.INFO) - app.logger.addHandler(file_handler) - app.logger.setLevel(logging.INFO) - app.logger.info('{{project_name}} startup') + logger.info( + \"Request started\", + request_id=g.request_id, + method=request.method, + path=request.path, + remote_addr=request.remote_addr, + user_agent=request.headers.get('User-Agent') + ) + + @app.after_request + def after_request(response): + response.headers['X-Request-ID'] = g.get('request_id', 'unknown') + + duration = time.time() - g.get('start_time', time.time()) + + logger.info( + \"Request completed\", + request_id=g.get('request_id'), + status_code=response.status_code, + duration_ms=round(duration * 1000, 2) + ) + + return response + + # Error handlers with structured logging + @app.errorhandler(404) + def not_found(error): + logger.warning( + \"Resource not found\", + request_id=g.get('request_id'), + path=request.path + ) + return {'error': 'Resource not found', 'request_id': g.get('request_id')}, 404 + + @app.errorhandler(500) + def internal_error(error): + logger.error( + \"Internal server error\", + request_id=g.get('request_id'), + error=str(error) + ) + return {'error': 'Internal server error', 'request_id': g.get('request_id')}, 500 # Register blueprints app.register_blueprint(health.bp) @@ -641,48 +1166,129 @@ def create_app(config_class=Config): @app.route('/') def index(): - return {'message': '{{project_name}} API is running', 'version': '1.0.0'} + return { + 'message': '{{project_name}} API is running', + 'version': '1.0.0', + 'environment': app.config.get('FLASK_ENV', 'unknown'), + 'request_id': g.get('request_id') + } + logger.info('{{project_name}} application created successfully') return app "#; - pub const CONFIG_PY: &str = r#"import os + pub const CONFIG_PY: &str = r#"from __future__ import annotations + +import os from datetime import timedelta +from typing import Dict, Type class Config: + \"\"\"Base configuration class with 2025 best practices\"\"\" + # Basic Flask configuration - SECRET_KEY = os.environ.get('SECRET_KEY') or '{{secret_key}}' + SECRET_KEY: str = os.environ.get('SECRET_KEY', '{{secret_key}}') - # Database - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://user:password@localhost/{{snake_case}}_db' - SQLALCHEMY_TRACK_MODIFICATIONS = False + # Application settings + PROJECT_NAME: str = '{{project_name}}' + VERSION: str = '1.0.0' - # JWT Configuration - JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') or '{{secret_key}}' - JWT_ACCESS_TOKEN_EXPIRES = timedelta(hours=int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRES', 1))) - JWT_REFRESH_TOKEN_EXPIRES = timedelta(days=int(os.environ.get('JWT_REFRESH_TOKEN_EXPIRES', 30))) + # Database configuration with connection pooling + SQLALCHEMY_DATABASE_URI: str = os.environ.get( + 'DATABASE_URL', + 'postgresql://user:password@localhost/{{snake_case}}_db' + ) + SQLALCHEMY_TRACK_MODIFICATIONS: bool = False + SQLALCHEMY_ENGINE_OPTIONS: Dict = { + 'pool_size': int(os.environ.get('DB_POOL_SIZE', '20')), + 'pool_timeout': int(os.environ.get('DB_POOL_TIMEOUT', '30')), + 'pool_recycle': int(os.environ.get('DB_POOL_RECYCLE', '3600')), + 'max_overflow': int(os.environ.get('DB_MAX_OVERFLOW', '0')) + } - # CORS - CORS_ORIGINS = os.environ.get('ALLOWED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000').split(',') + # JWT Configuration with enhanced security + JWT_SECRET_KEY: str = os.environ.get('JWT_SECRET_KEY', '{{secret_key}}') + JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta( + minutes=int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRES', '30')) + ) + JWT_REFRESH_TOKEN_EXPIRES: timedelta = timedelta( + days=int(os.environ.get('JWT_REFRESH_TOKEN_EXPIRES', '7')) + ) + JWT_ALGORITHM: str = 'HS256' + JWT_BLACKLIST_ENABLED: bool = True + JWT_BLACKLIST_TOKEN_CHECKS: list = ['access', 'refresh'] + + # CORS with enhanced security + CORS_ORIGINS: list = os.environ.get( + 'ALLOWED_ORIGINS', + 'http://localhost:3000,http://127.0.0.1:3000' + ).split(',') + CORS_METHODS: list = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] + CORS_ALLOW_HEADERS: list = ['Content-Type', 'Authorization'] # Rate limiting - RATELIMIT_STORAGE_URL = os.environ.get('REDIS_URL') or 'redis://localhost:6379' + RATELIMIT_STORAGE_URL: str = os.environ.get('REDIS_URL', 'redis://localhost:6379') + RATELIMIT_DEFAULT: str = \"200 per day, 50 per hour\" + + # Logging configuration + LOG_LEVEL: str = os.environ.get('LOG_LEVEL', 'INFO') + LOG_FORMAT: str = os.environ.get('LOG_FORMAT', 'json') + + # Security headers + SECURITY_HEADERS: Dict = { + 'X-Frame-Options': 'DENY', + 'X-Content-Type-Options': 'nosniff', + 'X-XSS-Protection': '1; mode=block', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'Content-Security-Policy': \"default-src 'self'\" + } # Environment - FLASK_ENV = os.environ.get('FLASK_ENV', 'development') - DEBUG = os.environ.get('FLASK_DEBUG', '0') == '1' + FLASK_ENV: str = os.environ.get('FLASK_ENV', 'development') + DEBUG: bool = os.environ.get('FLASK_DEBUG', '0') == '1' + TESTING: bool = False + + @property + def is_development(self) -> bool: + return self.FLASK_ENV == 'development' + + @property + def is_production(self) -> bool: + return self.FLASK_ENV == 'production' + + @property + def is_testing(self) -> bool: + return self.TESTING class DevelopmentConfig(Config): - DEBUG = True - + \"\"\"Development configuration\"\"\" + DEBUG: bool = True + LOG_LEVEL: str = 'DEBUG' + class ProductionConfig(Config): - DEBUG = False - + \"\"\"Production configuration with enhanced security\"\"\" + DEBUG: bool = False + TESTING: bool = False + + # Enhanced security for production + SESSION_COOKIE_SECURE: bool = True + SESSION_COOKIE_HTTPONLY: bool = True + SESSION_COOKIE_SAMESITE: str = 'Lax' + + # Force HTTPS in production + PREFERRED_URL_SCHEME: str = 'https' + class TestingConfig(Config): - TESTING = True - SQLALCHEMY_DATABASE_URI = 'sqlite:///:memory:' + \"\"\"Testing configuration\"\"\" + TESTING: bool = True + DEBUG: bool = True + SQLALCHEMY_DATABASE_URI: str = 'sqlite:///:memory:' + JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta(minutes=5) + + # Disable rate limiting in tests + RATELIMIT_ENABLED: bool = False -config = { +config: Dict[str, Type[Config]] = { 'development': DevelopmentConfig, 'production': ProductionConfig, 'testing': TestingConfig, @@ -1077,20 +1683,52 @@ def active_user_required(f): return decorated_function "#; - pub const REQUIREMENTS_TXT: &str = r#"Flask==3.0.0 -Flask-SQLAlchemy==3.1.1 -Flask-Migrate==4.0.5 -Flask-JWT-Extended==4.6.0 -Flask-CORS==4.0.0 -Flask-Limiter==3.5.0 -psycopg2-binary==2.9.9 -marshmallow==3.20.1 -Werkzeug==3.0.1 -gunicorn==21.2.0 -python-dotenv==1.0.0 -redis==5.0.1 -pytest==7.4.3 + pub const REQUIREMENTS_TXT: &str = r#"# Core Framework - Latest 2025 versions +Flask==3.1.0 +Flask-SQLAlchemy==3.2.0 +Flask-Migrate==4.1.0 +Flask-JWT-Extended==4.7.1 +Flask-CORS==5.0.0 +Flask-Limiter==3.8.0 + +# Database drivers +psycopg2-binary==2.9.10 +SQLAlchemy==2.0.36 + +# Structured Logging +structlog==24.4.0 +python-json-logger==2.0.7 + +# Serialization & Validation +marshmallow==3.23.0 +marshmallow-sqlalchemy==1.1.0 + +# Security & Password hashing +Werkzeug==3.1.0 +bcrypt==4.2.0 + +# Production server +gunicorn==23.0.0 + +# Configuration & Environment +python-dotenv==1.0.1 + +# Caching & Storage +redis==5.1.1 + +# Development & Testing +pytest==8.3.3 pytest-flask==1.3.0 +pytest-cov==5.0.0 +factory-boy==3.3.1 + +# Type checking & Code quality +mypy==1.13.0 +ruff==0.7.4 +pre-commit==4.0.1 + +# Monitoring (optional) +prometheus-flask-exporter==0.24.0 "#; pub const REQUIREMENTS_TXT_MYSQL: &str = r#"Flask==3.0.0 @@ -1113,7 +1751,7 @@ pytest-flask==1.3.0 pub const DOCKERFILE: &str = r#"# ========================= # Build stage # ========================= -FROM python:3.11-slim AS builder +FROM python:3.12-slim AS builder ENV VENV_PATH=/opt/venv RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -1612,4 +2250,72 @@ def test_duplicate_registration(client): data = response.get_json() assert data['message'] == 'User already exists' "#; + + // New structured logging module for Flask + pub const FLASK_LOGGING_PY: &str = r#"from __future__ import annotations + +import logging +import logging.config +import structlog +from typing import Dict, Any +from flask import Flask, has_request_context, g + +def setup_logging(app: Flask) -> None: + """Configure structured logging for Flask with structlog""" + + log_level = app.config.get('LOG_LEVEL', 'INFO') + log_format = app.config.get('LOG_FORMAT', 'json') + + # Configure structlog + if log_format == 'json': + processors = [ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt='iso'), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + structlog.processors.UnicodeDecoder(), + add_flask_context, + structlog.processors.JSONRenderer() + ] + else: + processors = [ + structlog.stdlib.filter_by_level, + structlog.stdlib.add_logger_name, + structlog.stdlib.add_log_level, + structlog.stdlib.PositionalArgumentsFormatter(), + structlog.processors.TimeStamper(fmt='%Y-%m-%d %H:%M:%S'), + structlog.processors.StackInfoRenderer(), + structlog.processors.format_exc_info, + add_flask_context, + structlog.dev.ConsoleRenderer() + ] + + structlog.configure( + processors=processors, + context_class=dict, + logger_factory=structlog.stdlib.LoggerFactory(), + wrapper_class=structlog.stdlib.BoundLogger, + cache_logger_on_first_use=True, + ) + + # Configure standard library logging + logging.basicConfig( + format='%(message)s', + level=getattr(logging, log_level.upper()), + force=True, + ) + + # Disable werkzeug logs in production + if not app.debug: + logging.getLogger('werkzeug').setLevel(logging.WARNING) + +def add_flask_context(logger, method_name: str, event_dict: Dict[str, Any]) -> Dict[str, Any]: + """Add Flask request context to log events""" + if has_request_context(): + event_dict['request_id'] = getattr(g, 'request_id', None) + return event_dict +"#; } \ No newline at end of file From ac998ac09680a80064deaef96dbc71f7809a5046 Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Wed, 15 Oct 2025 10:05:03 +0200 Subject: [PATCH 2/3] :sparkles: Add Boilerplate PHP= Laravel & Symphony --- docker-compose.yml | 30 +- docs/BOILERPLATE.md | 233 ++- src/boilerplate/mod.rs | 20 +- src/boilerplate/php.rs | 561 ++++++ src/boilerplate/templates.rs | 3671 ++++++++++++++++++++++++++++++++-- src/cli/args.rs | 34 + src/cli/commands.rs | 71 +- 7 files changed, 4431 insertions(+), 189 deletions(-) create mode 100644 src/boilerplate/php.rs diff --git a/docker-compose.yml b/docker-compose.yml index 53c7c3d..3246667 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,25 @@ # Generated by Athena v0.1.0 from test_no_conflicts deployment # Developed by UNFAIR Team: https://github.com/Jeck0v/Athena -# Generated: 2025-10-12 15:38:11 UTC +# Generated: 2025-10-15 08:04:01 UTC # Features: Intelligent defaults, optimized networking, enhanced health checks # Services: 3 configured with intelligent defaults services: - app1: - image: nginx:alpine - container_name: test-no-conflicts-app1 + app3: + image: apache:latest + container_name: test-no-conflicts-app3 ports: - - 8080:80 + - 9000:80 restart: always networks: - test_no_conflicts_network pull_policy: missing labels: + athena.service: app3 athena.project: test_no_conflicts athena.type: proxy - athena.service: app1 - athena.generated: 2025-10-12 + athena.generated: 2025-10-15 app2: image: httpd:alpine @@ -31,25 +31,25 @@ services: - test_no_conflicts_network pull_policy: missing labels: + athena.service: app2 athena.project: test_no_conflicts - athena.generated: 2025-10-12 athena.type: generic - athena.service: app2 + athena.generated: 2025-10-15 - app3: - image: apache:latest - container_name: test-no-conflicts-app3 + app1: + image: nginx:alpine + container_name: test-no-conflicts-app1 ports: - - 9000:80 + - 8080:80 restart: always networks: - test_no_conflicts_network pull_policy: missing labels: + athena.generated: 2025-10-15 athena.project: test_no_conflicts + athena.service: app1 athena.type: proxy - athena.service: app3 - athena.generated: 2025-10-12 networks: test_no_conflicts_network: driver: bridge diff --git a/docs/BOILERPLATE.md b/docs/BOILERPLATE.md index a90a8d3..01b6b56 100644 --- a/docs/BOILERPLATE.md +++ b/docs/BOILERPLATE.md @@ -76,9 +76,232 @@ my-service/ ## Flask Projects ```bash -# Flask + PostgreSQL -athena init flask my-app --with-postgresql +# Flask + PostgreSQL (default) +athena init flask my-app -# Flask + MongoDB -athena init flask my-app --with-mongodb -``` \ No newline at end of file +# Flask + MySQL +athena init flask my-app --with-mysql + +# Without Docker files +athena init flask my-app --no-docker +``` + +**Generated Flask Structure:** +``` +my-app/ +├── app/ +│ ├── __init__.py # Flask application factory +│ ├── core/ +│ │ ├── config.py # Configuration management +│ │ ├── extensions.py # Flask extensions +│ │ └── logging.py # Structured logging +│ ├── api/ +│ │ ├── health.py # Health check endpoints +│ │ └── v1/ # API versioning +│ │ ├── auth.py # JWT authentication +│ │ └── users.py # User management +│ ├── models/ # SQLAlchemy models +│ ├── schemas/ # Marshmallow schemas +│ └── services/ # Business logic layer +├── tests/ # Comprehensive test suite +├── nginx/ # Reverse proxy config +├── requirements.txt # Python dependencies +├── Dockerfile # Multi-stage production build +├── docker-compose.yml # Full stack deployment +└── .env.example # Environment template +``` + +## Laravel Projects (Clean Architecture) +```bash +# Laravel + PostgreSQL (default) +athena init laravel my-project + +# Laravel + MySQL +athena init laravel my-project --with-mysql + +# Without Docker files +athena init laravel my-project --no-docker +``` + +**Generated Laravel Structure:** +``` +my-project/ +├── app/ +│ ├── Domain/ # Domain layer (Clean Architecture) +│ │ └── User/ +│ │ ├── Entities/ # Domain entities +│ │ │ └── User.php # User entity with business logic +│ │ ├── Repositories/ # Repository interfaces +│ │ └── Services/ # Domain services +│ ├── Application/ # Application layer +│ │ └── User/ +│ │ ├── UseCases/ # Use cases (business logic) +│ │ ├── DTOs/ # Data Transfer Objects +│ │ └── Services/ # Application services +│ └── Infrastructure/ # Infrastructure layer +│ ├── Http/ +│ │ ├── Controllers/ # API controllers +│ │ └── Middleware/ # Custom middleware +│ ├── Persistence/ # Data persistence +│ │ ├── Eloquent/ # Eloquent models +│ │ └── Repositories/ # Repository implementations +│ └── Providers/ # Service providers +├── config/ # Laravel configuration +├── database/ +│ ├── migrations/ # Database migrations +│ └── seeders/ # Data seeders +├── tests/ # Feature & Unit tests +├── docker/ # Docker configurations +├── nginx/ # Nginx configuration +├── composer.json # PHP dependencies (Laravel 11, PHP 8.2) +├── Dockerfile # Multi-stage production build +├── docker-compose.yml # Full stack deployment +└── .env.example # Environment template +``` + +## Symfony Projects (Hexagonal Architecture) +```bash +# Symfony + PostgreSQL (default) +athena init symfony my-api + +# Symfony + MySQL +athena init symfony my-api --with-mysql + +# Without Docker files +athena init symfony my-api --no-docker +``` + +**Generated Symfony Structure:** +``` +my-api/ +├── src/ +│ ├── Domain/ # Domain layer (Hexagonal Architecture) +│ │ └── User/ +│ │ ├── Entities/ # Domain entities +│ │ │ └── User.php # Pure domain entity +│ │ ├── ValueObjects/ # Value objects +│ │ │ ├── UserId.php # User ID value object +│ │ │ ├── Email.php # Email value object +│ │ │ ├── UserName.php # User name value object +│ │ │ └── HashedPassword.php +│ │ └── Repositories/ # Repository interfaces +│ │ └── UserRepositoryInterface.php +│ ├── Application/ # Application layer +│ │ └── User/ +│ │ ├── Commands/ # CQRS Commands +│ │ │ ├── CreateUserCommand.php +│ │ │ └── LoginCommand.php +│ │ ├── Queries/ # CQRS Queries +│ │ │ └── GetUserQuery.php +│ │ ├── Handlers/ # Command/Query handlers +│ │ │ ├── UserHandler.php +│ │ │ └── AuthHandler.php +│ │ └── Services/ # Application services +│ │ ├── UserService.php +│ │ └── AuthService.php +│ └── Infrastructure/ # Infrastructure layer +│ ├── Http/ +│ │ └── Controllers/ # API controllers +│ │ └── UserController.php +│ └── Persistence/ +│ └── Doctrine/ +│ ├── Entities/ # Doctrine entities +│ │ └── User.php # Infrastructure User entity +│ └── Repositories/ # Repository implementations +│ └── DoctrineUserRepository.php +├── config/ # Symfony configuration +├── migrations/ # Doctrine migrations +├── tests/ # Functional & Unit tests +├── docker/ # Docker configurations +├── nginx/ # Nginx configuration +├── composer.json # PHP dependencies (Symfony 7, PHP 8.2) +├── Dockerfile # Multi-stage production build +├── docker-compose.yml # Full stack deployment +└── .env.example # Environment template +``` + +## Features & Best Practices 2025 + +### **Architecture Patterns** +- **Laravel**: Clean Architecture with Domain/Application/Infrastructure layers +- **Symfony**: Hexagonal Architecture with CQRS pattern +- **FastAPI**: Async-first architecture with dependency injection +- **Flask**: Layered architecture with factory pattern +- **Go**: Clean architecture with interfaces and dependency injection + +### **Security & Authentication** +- **JWT Authentication** with refresh tokens +- **Password hashing** with modern algorithms (bcrypt/argon2) +- **CORS configuration** for cross-origin requests +- **Input validation** and sanitization +- **Security headers** in Nginx configuration +- **Environment-based secrets** management + +### **Modern Language Features** +- **PHP 8.2+**: Strict types, readonly properties, attributes +- **Python 3.12+**: Type hints, async/await, dataclasses +- **Go 1.22+**: Generics, structured logging with slog +- **Dependency injection** and inversion of control +- **Value objects** and domain-driven design + +### **Production-Ready Infrastructure** +- **Multi-stage Dockerfiles** for optimized builds +- **Nginx reverse proxy** with caching and compression +- **Health checks** and monitoring endpoints +- **Structured logging** with correlation IDs +- **Database migrations** and seeding +- **Redis caching** integration + +### **Testing & Quality** +- **Comprehensive test suites** (unit, integration, functional) +- **PHPUnit 10** / **pytest** / **testify** frameworks +- **Code quality tools**: PHPStan, mypy, golangci-lint +- **Code formatting**: PHP-CS-Fixer, black, gofmt +- **Test coverage** reporting +- **CI/CD ready** configurations + +### **Development Experience** +- **Hot reload** in development environments +- **Environment-based configuration** (.env files) +- **Database GUI tools** (Adminer/phpMyAdmin) +- **API documentation** with OpenAPI/Swagger +- **Pre-commit hooks** for code quality +- **Development scripts** and automation + +## Quick Start Example + +```bash +# Create a modern Laravel API +athena init laravel my-laravel-api +cd my-laravel-api +cp .env.example .env + +# Start with Docker +docker-compose up --build + +# Install dependencies and migrate +docker-compose exec app composer install +docker-compose exec app php artisan migrate + +# Test the API +curl http://localhost/api/health +``` + +```bash +# Create a Symfony hexagonal API +athena init symfony my-symfony-api --with-mysql +cd my-symfony-api +cp .env.example .env + +# Start with Docker +docker-compose up --build + +# Install dependencies and migrate +docker-compose exec app composer install +docker-compose exec app php bin/console doctrine:migrations:migrate + +# Test the API +curl http://localhost/api/health +``` + +All generated projects include comprehensive README files with setup instructions, API documentation, and deployment guides. diff --git a/src/boilerplate/mod.rs b/src/boilerplate/mod.rs index 7f4be1d..d021f6b 100644 --- a/src/boilerplate/mod.rs +++ b/src/boilerplate/mod.rs @@ -1,15 +1,17 @@ -//! Boilerplate generation module for FastAPI, Flask, and Go projects +//! Boilerplate generation module for FastAPI, Flask, Go, and PHP projects //! //! This module provides production-ready project templates with: //! - Authentication systems (JWT with refresh tokens) //! - Security best practices (bcrypt/argon2, AES-256) -//! - Database integration (MongoDB/PostgreSQL) +//! - Database integration (MongoDB/PostgreSQL/MySQL) //! - Docker containerization //! - Nginx reverse proxy configuration +//! - Clean Architecture and DDD patterns pub mod fastapi; pub mod flask; pub mod go; +pub mod php; pub mod templates; pub mod utils; @@ -68,6 +70,20 @@ pub fn generate_go_project(config: &ProjectConfig) -> BoilerplateResult<()> { generator.generate_project(config) } +/// Generate a Laravel PHP boilerplate project +pub fn generate_laravel_project(config: &ProjectConfig) -> BoilerplateResult<()> { + let generator = php::PhpGenerator::new(); + generator.validate_config(config)?; + generator.generate_laravel_project(config) +} + +/// Generate a Symfony PHP boilerplate project +pub fn generate_symfony_project(config: &ProjectConfig) -> BoilerplateResult<()> { + let generator = php::PhpGenerator::new(); + generator.validate_config(config)?; + generator.generate_symfony_project(config) +} + /// Validate project name pub fn validate_project_name(name: &str) -> BoilerplateResult<()> { if name.is_empty() { diff --git a/src/boilerplate/php.rs b/src/boilerplate/php.rs new file mode 100644 index 0000000..490dc07 --- /dev/null +++ b/src/boilerplate/php.rs @@ -0,0 +1,561 @@ +//! PHP boilerplate generator with Laravel and Symfony support using Clean Architecture + +use crate::boilerplate::{BoilerplateGenerator, BoilerplateResult, ProjectConfig}; +use crate::boilerplate::utils::{create_directory_structure, write_file, replace_template_vars_string, generate_secret_key, ProjectNames}; +use crate::boilerplate::templates::php::*; +use std::path::Path; + +/// Simple base64 encoding (without external dependency) +fn base64_encode(input: &str) -> String { + const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + let mut result = String::new(); + let bytes = input.as_bytes(); + + for chunk in bytes.chunks(3) { + let mut buf = [0u8; 3]; + for (i, &byte) in chunk.iter().enumerate() { + buf[i] = byte; + } + + let b = ((buf[0] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[2] as u32); + + result.push(ALPHABET[((b >> 18) & 63) as usize] as char); + result.push(ALPHABET[((b >> 12) & 63) as usize] as char); + result.push(if chunk.len() > 1 { ALPHABET[((b >> 6) & 63) as usize] as char } else { '=' }); + result.push(if chunk.len() > 2 { ALPHABET[(b & 63) as usize] as char } else { '=' }); + } + + result +} + +pub struct PhpGenerator; + +#[derive(Debug, Clone)] +pub enum PhpFramework { + Laravel, + Symfony, +} + +impl Default for PhpGenerator { + fn default() -> Self { + Self::new() + } +} + +impl PhpGenerator { + pub fn new() -> Self { + Self + } + + fn get_template_vars(&self, config: &ProjectConfig) -> Vec<(&str, String)> { + let names = ProjectNames::new(&config.name); + let secret_key = generate_secret_key(); + + vec![ + ("project_name", config.name.clone()), + ("snake_case", names.snake_case.clone()), + ("kebab_case", names.kebab_case.clone()), + ("pascal_case", names.pascal_case), + ("upper_case", names.upper_case), + ("secret_key", secret_key), + ("app_key", format!("base64:{}", base64_encode(&generate_secret_key()))), + ] + } + + fn create_laravel_structure(&self, base_path: &Path) -> BoilerplateResult<()> { + let directories = vec![ + // Laravel-specific clean architecture structure + "app/Domain/User/Entities", + "app/Domain/User/Repositories", + "app/Domain/User/Services", + "app/Domain/User/ValueObjects", + "app/Domain/Auth/Services", + "app/Application/User/Commands", + "app/Application/User/Queries", + "app/Application/User/Handlers", + "app/Application/Auth/Commands", + "app/Application/Auth/Handlers", + "app/Infrastructure/Persistence/Eloquent", + "app/Infrastructure/Http/Controllers/Api/V1", + "app/Infrastructure/Http/Middleware", + "app/Infrastructure/Http/Requests", + "app/Infrastructure/Http/Resources", + "app/Infrastructure/Providers", + "app/Infrastructure/Exceptions", + "app/Shared/Events", + "app/Shared/Notifications", + "app/Shared/Services", + "config", + "database/migrations", + "database/seeders", + "routes", + "tests/Unit/Domain", + "tests/Unit/Application", + "tests/Feature/Api", + "tests/Integration", + "storage/logs", + "public", + "resources/lang", + "docker/nginx", + "docker/php", + ]; + + create_directory_structure(base_path, &directories) + } + + fn create_symfony_structure(&self, base_path: &Path) -> BoilerplateResult<()> { + let directories = vec![ + // Symfony-specific hexagonal architecture structure + "src/Domain/User/Entity", + "src/Domain/User/Repository", + "src/Domain/User/Service", + "src/Domain/User/ValueObject", + "src/Domain/Auth/Service", + "src/Application/User/Command", + "src/Application/User/Query", + "src/Application/User/Handler", + "src/Application/Auth/Command", + "src/Application/Auth/Handler", + "src/Infrastructure/Persistence/Doctrine", + "src/Infrastructure/Http/Controller/Api/V1", + "src/Infrastructure/Http/EventListener", + "src/Infrastructure/Security", + "src/Infrastructure/Serializer", + "src/Shared/Event", + "src/Shared/Service", + "config/packages", + "config/routes", + "migrations", + "tests/Unit/Domain", + "tests/Unit/Application", + "tests/Integration/Infrastructure", + "tests/Functional/Api", + "var/log", + "public", + "translations", + "docker/nginx", + "docker/php", + "templates", + ]; + + create_directory_structure(base_path, &directories) + } + + + fn generate_laravel_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // Generate Laravel-specific files + let composer_json = replace_template_vars_string(LARAVEL_COMPOSER_JSON, &vars); + write_file(base_path.join("composer.json"), &composer_json)?; + + let artisan = replace_template_vars_string(LARAVEL_ARTISAN, &vars); + write_file(base_path.join("artisan"), &artisan)?; + + // Laravel configuration files + let app_config = replace_template_vars_string(LARAVEL_APP_CONFIG, &vars); + write_file(base_path.join("config/app.php"), &app_config)?; + + let database_config = replace_template_vars_string(LARAVEL_DATABASE_CONFIG, &vars); + write_file(base_path.join("config/database.php"), &database_config)?; + + let auth_config = replace_template_vars_string(LARAVEL_AUTH_CONFIG, &vars); + write_file(base_path.join("config/auth.php"), &auth_config)?; + + // Clean Architecture - Domain Layer + self.generate_laravel_domain_layer(config, base_path)?; + + // Clean Architecture - Application Layer + self.generate_laravel_application_layer(config, base_path)?; + + // Clean Architecture - Infrastructure Layer + self.generate_laravel_infrastructure_layer(config, base_path)?; + + Ok(()) + } + + fn generate_laravel_domain_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // User Entity + let user_entity = replace_template_vars_string(LARAVEL_USER_ENTITY, &vars); + write_file(base_path.join("app/Domain/User/Entities/User.php"), &user_entity)?; + + // User Repository Interface + let user_repository = replace_template_vars_string(LARAVEL_USER_REPOSITORY, &vars); + write_file(base_path.join("app/Domain/User/Repositories/UserRepositoryInterface.php"), &user_repository)?; + + // User Service + let user_service = replace_template_vars_string(LARAVEL_USER_SERVICE, &vars); + write_file(base_path.join("app/Domain/User/Services/UserService.php"), &user_service)?; + + // Auth Service + let auth_service = replace_template_vars_string(LARAVEL_AUTH_SERVICE, &vars); + write_file(base_path.join("app/Domain/Auth/Services/AuthService.php"), &auth_service)?; + + Ok(()) + } + + fn generate_laravel_application_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // User Commands + let create_user_command = replace_template_vars_string(LARAVEL_CREATE_USER_COMMAND, &vars); + write_file(base_path.join("app/Application/User/Commands/CreateUserCommand.php"), &create_user_command)?; + + // User Queries + let get_user_query = replace_template_vars_string(LARAVEL_GET_USER_QUERY, &vars); + write_file(base_path.join("app/Application/User/Queries/GetUserQuery.php"), &get_user_query)?; + + // User Handlers + let user_handler = replace_template_vars_string(LARAVEL_USER_HANDLER, &vars); + write_file(base_path.join("app/Application/User/Handlers/UserHandler.php"), &user_handler)?; + + // Auth Commands & Handlers + let login_command = replace_template_vars_string(LARAVEL_LOGIN_COMMAND, &vars); + write_file(base_path.join("app/Application/Auth/Commands/LoginCommand.php"), &login_command)?; + + let auth_handler = replace_template_vars_string(LARAVEL_AUTH_HANDLER, &vars); + write_file(base_path.join("app/Application/Auth/Handlers/AuthHandler.php"), &auth_handler)?; + + Ok(()) + } + + fn generate_laravel_infrastructure_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // Eloquent Repository Implementation + let eloquent_user_repository = replace_template_vars_string(LARAVEL_ELOQUENT_USER_REPOSITORY, &vars); + write_file(base_path.join("app/Infrastructure/Persistence/Eloquent/EloquentUserRepository.php"), &eloquent_user_repository)?; + + // API Controllers + let user_controller = replace_template_vars_string(LARAVEL_USER_CONTROLLER, &vars); + write_file(base_path.join("app/Infrastructure/Http/Controllers/Api/V1/UserController.php"), &user_controller)?; + + let auth_controller = replace_template_vars_string(LARAVEL_AUTH_CONTROLLER, &vars); + write_file(base_path.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php"), &auth_controller)?; + + // HTTP Requests + let register_request = replace_template_vars_string(LARAVEL_REGISTER_REQUEST, &vars); + write_file(base_path.join("app/Infrastructure/Http/Requests/RegisterRequest.php"), ®ister_request)?; + + let login_request = replace_template_vars_string(LARAVEL_LOGIN_REQUEST, &vars); + write_file(base_path.join("app/Infrastructure/Http/Requests/LoginRequest.php"), &login_request)?; + + // API Routes + let api_routes = replace_template_vars_string(LARAVEL_API_ROUTES, &vars); + write_file(base_path.join("routes/api.php"), &api_routes)?; + + // Service Provider + let app_service_provider = replace_template_vars_string(LARAVEL_APP_SERVICE_PROVIDER, &vars); + write_file(base_path.join("app/Infrastructure/Providers/AppServiceProvider.php"), &app_service_provider)?; + + Ok(()) + } + + fn generate_symfony_files(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // Generate Symfony-specific files + let composer_json = replace_template_vars_string(SYMFONY_COMPOSER_JSON, &vars); + write_file(base_path.join("composer.json"), &composer_json)?; + + // Symfony configuration + let services_yaml = replace_template_vars_string(SYMFONY_SERVICES_YAML, &vars); + write_file(base_path.join("config/services.yaml"), &services_yaml)?; + + let doctrine_yaml = replace_template_vars_string(SYMFONY_DOCTRINE_CONFIG, &vars); + write_file(base_path.join("config/packages/doctrine.yaml"), &doctrine_yaml)?; + + let security_yaml = replace_template_vars_string(SYMFONY_SECURITY_CONFIG, &vars); + write_file(base_path.join("config/packages/security.yaml"), &security_yaml)?; + + // Hexagonal Architecture - Domain Layer + self.generate_symfony_domain_layer(config, base_path)?; + + // Hexagonal Architecture - Application Layer + self.generate_symfony_application_layer(config, base_path)?; + + // Hexagonal Architecture - Infrastructure Layer + self.generate_symfony_infrastructure_layer(config, base_path)?; + + Ok(()) + } + + fn generate_symfony_domain_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // User Entity + let user_entity = replace_template_vars_string(SYMFONY_USER_ENTITY, &vars); + write_file(base_path.join("src/Domain/User/Entity/User.php"), &user_entity)?; + + // User Repository Interface + let user_repository = replace_template_vars_string(SYMFONY_USER_REPOSITORY, &vars); + write_file(base_path.join("src/Domain/User/Repository/UserRepositoryInterface.php"), &user_repository)?; + + // User Service + let user_service = replace_template_vars_string(SYMFONY_USER_SERVICE, &vars); + write_file(base_path.join("src/Domain/User/Service/UserService.php"), &user_service)?; + + // Auth Service + let auth_service = replace_template_vars_string(SYMFONY_AUTH_SERVICE, &vars); + write_file(base_path.join("src/Domain/Auth/Service/AuthService.php"), &auth_service)?; + + Ok(()) + } + + fn generate_symfony_application_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // User Commands + let create_user_command = replace_template_vars_string(SYMFONY_CREATE_USER_COMMAND, &vars); + write_file(base_path.join("src/Application/User/Command/CreateUserCommand.php"), &create_user_command)?; + + // User Queries + let get_user_query = replace_template_vars_string(SYMFONY_GET_USER_QUERY, &vars); + write_file(base_path.join("src/Application/User/Query/GetUserQuery.php"), &get_user_query)?; + + // User Handlers + let user_handler = replace_template_vars_string(SYMFONY_USER_HANDLER, &vars); + write_file(base_path.join("src/Application/User/Handler/UserHandler.php"), &user_handler)?; + + // Auth Commands & Handlers + let login_command = replace_template_vars_string(SYMFONY_LOGIN_COMMAND, &vars); + write_file(base_path.join("src/Application/Auth/Command/LoginCommand.php"), &login_command)?; + + let auth_handler = replace_template_vars_string(SYMFONY_AUTH_HANDLER, &vars); + write_file(base_path.join("src/Application/Auth/Handler/AuthHandler.php"), &auth_handler)?; + + Ok(()) + } + + fn generate_symfony_infrastructure_layer(&self, config: &ProjectConfig, base_path: &Path) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + // Doctrine Repository Implementation + let doctrine_user_repository = replace_template_vars_string(SYMFONY_DOCTRINE_USER_REPOSITORY, &vars); + write_file(base_path.join("src/Infrastructure/Persistence/Doctrine/DoctrineUserRepository.php"), &doctrine_user_repository)?; + + // API Controllers + let user_controller = replace_template_vars_string(SYMFONY_USER_CONTROLLER, &vars); + write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/UserController.php"), &user_controller)?; + + let auth_controller = replace_template_vars_string(SYMFONY_AUTH_CONTROLLER, &vars); + write_file(base_path.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php"), &auth_controller)?; + + // API Routes + let api_routes = replace_template_vars_string(SYMFONY_API_ROUTES, &vars); + write_file(base_path.join("config/routes/api.yaml"), &api_routes)?; + + Ok(()) + } + + fn generate_docker_files(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { + if !config.include_docker { + return Ok(()); + } + + let vars = self.get_template_vars(config); + + // Optimized PHP Dockerfile + let dockerfile_content = replace_template_vars_string(PHP_OPTIMIZED_DOCKERFILE, &vars); + write_file(base_path.join("docker/php/Dockerfile"), &dockerfile_content)?; + + // Nginx Dockerfile + let nginx_dockerfile = replace_template_vars_string(PHP_NGINX_DOCKERFILE, &vars); + write_file(base_path.join("docker/nginx/Dockerfile"), &nginx_dockerfile)?; + + // Docker Compose (production-ready) + let docker_compose = match framework { + PhpFramework::Laravel => replace_template_vars_string(LARAVEL_DOCKER_COMPOSE, &vars), + PhpFramework::Symfony => replace_template_vars_string(SYMFONY_DOCKER_COMPOSE, &vars), + }; + write_file(base_path.join("docker-compose.yml"), &docker_compose)?; + + // Docker Compose Development Override + let docker_compose_dev = replace_template_vars_string(PHP_DOCKER_COMPOSE_DEV, &vars); + write_file(base_path.join("docker-compose.dev.yml"), &docker_compose_dev)?; + + // Environment files + let env_docker = replace_template_vars_string(PHP_ENV_DOCKER, &vars); + write_file(base_path.join(".env.docker.example"), &env_docker)?; + + let env_example = match framework { + PhpFramework::Laravel => replace_template_vars_string(LARAVEL_ENV_EXAMPLE, &vars), + PhpFramework::Symfony => replace_template_vars_string(SYMFONY_ENV_EXAMPLE, &vars), + }; + write_file(base_path.join(".env.example"), &env_example)?; + + // Nginx configuration + let nginx_conf = replace_template_vars_string(PHP_NGINX_CONF, &vars); + write_file(base_path.join("docker/nginx/nginx.conf"), &nginx_conf)?; + + let nginx_default_conf = match framework { + PhpFramework::Laravel => replace_template_vars_string(LARAVEL_NGINX_DEFAULT_CONF, &vars), + PhpFramework::Symfony => replace_template_vars_string(SYMFONY_NGINX_DEFAULT_CONF, &vars), + }; + write_file(base_path.join("docker/nginx/default.conf"), &nginx_default_conf)?; + + // PHP-FPM configuration + let php_fpm_conf = replace_template_vars_string(PHP_FPM_CONF, &vars); + write_file(base_path.join("docker/php/php-fpm.conf"), &php_fpm_conf)?; + + Ok(()) + } + + fn generate_test_files(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + match framework { + PhpFramework::Laravel => { + let phpunit_xml = replace_template_vars_string(LARAVEL_PHPUNIT_XML, &vars); + write_file(base_path.join("phpunit.xml"), &phpunit_xml)?; + + let feature_test = replace_template_vars_string(LARAVEL_AUTH_FEATURE_TEST, &vars); + write_file(base_path.join("tests/Feature/Api/AuthTest.php"), &feature_test)?; + + let unit_test = replace_template_vars_string(LARAVEL_USER_UNIT_TEST, &vars); + write_file(base_path.join("tests/Unit/Domain/UserTest.php"), &unit_test)?; + } + PhpFramework::Symfony => { + let phpunit_xml = replace_template_vars_string(SYMFONY_PHPUNIT_XML, &vars); + write_file(base_path.join("phpunit.xml.dist"), &phpunit_xml)?; + + let functional_test = replace_template_vars_string(SYMFONY_AUTH_FUNCTIONAL_TEST, &vars); + write_file(base_path.join("tests/Functional/Api/AuthTest.php"), &functional_test)?; + + let unit_test = replace_template_vars_string(SYMFONY_USER_UNIT_TEST, &vars); + write_file(base_path.join("tests/Unit/Domain/UserTest.php"), &unit_test)?; + } + } + + Ok(()) + } + + fn generate_documentation(&self, config: &ProjectConfig, base_path: &Path, framework: &PhpFramework) -> BoilerplateResult<()> { + let vars = self.get_template_vars(config); + + let readme = match framework { + PhpFramework::Laravel => replace_template_vars_string(LARAVEL_README, &vars), + PhpFramework::Symfony => replace_template_vars_string(SYMFONY_README, &vars), + }; + + write_file(base_path.join("README.md"), &readme)?; + + Ok(()) + } + + pub fn generate_laravel_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { + let base_path = Path::new(&config.directory); + let framework = PhpFramework::Laravel; + + println!("Generating Laravel project with Clean Architecture: {}", config.name); + + // Create directory structure + println!(" 📁 Creating clean architecture structure..."); + self.create_laravel_structure(base_path)?; + + // Generate Laravel files + println!(" ⚡ Generating Laravel application files..."); + self.generate_laravel_files(config, base_path)?; + + // Generate Docker files + if config.include_docker { + println!(" 🐳 Generating Docker configuration..."); + self.generate_docker_files(config, base_path, &framework)?; + } + + // Generate test files + println!(" 🧪 Creating test suite..."); + self.generate_test_files(config, base_path, &framework)?; + + // Generate documentation + println!(" 📚 Generating documentation..."); + self.generate_documentation(config, base_path, &framework)?; + + println!("Laravel project '{}' created successfully!", config.name); + println!("📍 Location: {}", base_path.display()); + + if config.include_docker { + println!("\n🔧 Next steps:"); + println!(" cd {}", config.directory); + println!(" cp .env.example .env # Edit with your configuration"); + println!(" docker-compose up --build"); + println!(" docker-compose exec app composer install"); + println!(" docker-compose exec app php artisan migrate"); + } else { + println!("\n🔧 Next steps:"); + println!(" cd {}", config.directory); + println!(" composer install"); + println!(" cp .env.example .env # Edit with your configuration"); + println!(" php artisan key:generate"); + println!(" php artisan migrate"); + println!(" php artisan serve"); + } + + Ok(()) + } + + pub fn generate_symfony_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { + let base_path = Path::new(&config.directory); + let framework = PhpFramework::Symfony; + + println!("Generating Symfony project with Hexagonal Architecture: {}", config.name); + + // Create directory structure + println!(" 📁 Creating hexagonal architecture structure..."); + self.create_symfony_structure(base_path)?; + + // Generate Symfony files + println!(" 🎼 Generating Symfony application files..."); + self.generate_symfony_files(config, base_path)?; + + // Generate Docker files + if config.include_docker { + println!(" 🐳 Generating Docker configuration..."); + self.generate_docker_files(config, base_path, &framework)?; + } + + // Generate test files + println!(" 🧪 Creating test suite..."); + self.generate_test_files(config, base_path, &framework)?; + + // Generate documentation + println!(" 📚 Generating documentation..."); + self.generate_documentation(config, base_path, &framework)?; + + println!("Symfony project '{}' created successfully!", config.name); + println!("📍 Location: {}", base_path.display()); + + if config.include_docker { + println!("\n🔧 Next steps:"); + println!(" cd {}", config.directory); + println!(" cp .env.example .env # Edit with your configuration"); + println!(" docker-compose up --build"); + println!(" docker-compose exec app composer install"); + println!(" docker-compose exec app php bin/console doctrine:migrations:migrate"); + } else { + println!("\n🔧 Next steps:"); + println!(" cd {}", config.directory); + println!(" composer install"); + println!(" cp .env.example .env # Edit with your configuration"); + println!(" php bin/console doctrine:migrations:migrate"); + println!(" symfony server:start"); + } + + Ok(()) + } +} + +impl BoilerplateGenerator for PhpGenerator { + fn validate_config(&self, config: &ProjectConfig) -> BoilerplateResult<()> { + crate::boilerplate::validate_project_name(&config.name)?; + crate::boilerplate::check_directory_availability(Path::new(&config.directory))?; + Ok(()) + } + + fn generate_project(&self, config: &ProjectConfig) -> BoilerplateResult<()> { + // Default to Laravel, but this can be extended to support framework selection + self.generate_laravel_project(config) + } +} \ No newline at end of file diff --git a/src/boilerplate/templates.rs b/src/boilerplate/templates.rs index 97a58c8..720873b 100644 --- a/src/boilerplate/templates.rs +++ b/src/boilerplate/templates.rs @@ -100,7 +100,7 @@ async def root(request: Request) -> dict[str, Any]: request_id = getattr(request.state, "request_id", str(uuid.uuid4())) logger.info("Root endpoint accessed", request_id=request_id) return { - "message": "{{project_name}} API is running", + "message": "{{project_name}} API is running", "version": "1.0.0", "environment": settings.ENVIRONMENT, "request_id": request_id @@ -130,13 +130,13 @@ class Settings(BaseSettings): VERSION: str = "1.0.0" ENVIRONMENT: str = Field(default="development", description="Environment: development, staging, production") DEBUG: bool = Field(default=False, description="Enable debug mode") - + # Security SECRET_KEY: str = Field("{{secret_key}}", min_length=32, description="Secret key for JWT signing") ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30, ge=5, le=1440) REFRESH_TOKEN_EXPIRE_MINUTES: int = Field(default=60 * 24 * 7, ge=60) # 7 days ALGORITHM: str = "HS256" - + # CORS and Security ALLOWED_HOSTS: List[str] = Field( default=["http://localhost:3000", "http://127.0.0.1:3000"], @@ -146,62 +146,62 @@ class Settings(BaseSettings): default=["localhost", "127.0.0.1"], description="Trusted hosts for TrustedHostMiddleware" ) - + # Database MONGODB_URL: str = "mongodb://localhost:27017" DATABASE_NAME: str = "{{snake_case}}_db" DATABASE_URL: str = "postgresql+asyncpg://user:password@localhost/{{snake_case}}_db" POSTGRES_PASSWORD: str = "changeme" - + # Connection Pool Settings DB_POOL_SIZE: int = Field(default=20, ge=5, le=100) DB_POOL_OVERFLOW: int = Field(default=0, ge=0, le=50) - + # Redis (for caching/sessions) REDIS_URL: str = "redis://localhost:6379" - + # Logging LOG_LEVEL: str = Field(default="INFO", description="Logging level") LOG_FORMAT: str = Field(default="json", description="Logging format: json or text") - + # Rate Limiting RATE_LIMIT_ENABLED: bool = Field(default=True, description="Enable rate limiting") RATE_LIMIT_CALLS: int = Field(default=100, ge=1, description="Rate limit calls per period") RATE_LIMIT_PERIOD: int = Field(default=60, ge=1, description="Rate limit period in seconds") - + # OpenTelemetry (optional) OTEL_ENABLED: bool = Field(default=False, description="Enable OpenTelemetry") OTEL_ENDPOINT: str = Field(default="", description="OpenTelemetry endpoint") - + @validator("ENVIRONMENT") def validate_environment(cls, v: str) -> str: if v not in ["development", "staging", "production"]: raise ValueError("ENVIRONMENT must be development, staging, or production") return v - + @validator("LOG_LEVEL") def validate_log_level(cls, v: str) -> str: if v.upper() not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]: raise ValueError("LOG_LEVEL must be DEBUG, INFO, WARNING, ERROR, or CRITICAL") return v.upper() - + @validator("LOG_FORMAT") def validate_log_format(cls, v: str) -> str: if v not in ["json", "text"]: raise ValueError("LOG_FORMAT must be json or text") return v - + model_config = ConfigDict( env_file=".env", case_sensitive=True, validate_assignment=True, extra="forbid" ) - + @property def is_development(self) -> bool: return self.ENVIRONMENT == "development" - + @property def is_production(self) -> bool: return self.ENVIRONMENT == "production" @@ -234,7 +234,7 @@ def create_access_token(data: Dict[str, Any]) -> str: to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire, "type": "access"}) - + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt @@ -243,7 +243,7 @@ def create_refresh_token(data: Dict[str, Any]) -> str: to_encode = data.copy() expire = datetime.utcnow() + timedelta(minutes=settings.REFRESH_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire, "type": "refresh"}) - + encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt @@ -251,14 +251,14 @@ def verify_token(token: str, token_type: str = "access") -> Dict[str, Any]: """Verify and decode JWT token""" try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) - + # Verify token type if payload.get("type") != token_type: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type" ) - + return payload except JWTError: raise HTTPException( @@ -471,7 +471,7 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - + # Basic settings sendfile on; tcp_nopush on; @@ -479,17 +479,17 @@ http { keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 16M; - + # Security headers add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always; - + # Rate limiting limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - + # Gzip compression gzip on; gzip_vary on; @@ -504,14 +504,14 @@ http { application/javascript application/xml+rss application/atom+xml; - + # Logging format log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - + access_log /var/log/nginx/access.log main; - + # Include server configurations include /etc/nginx/conf.d/*.conf; } @@ -526,10 +526,10 @@ upstream {{kebab_case}}_api { server { listen 80; server_name _; - + # Apply rate limiting limit_req zone=api burst=20 nodelay; - + # API proxy location /api/ { proxy_pass http://{{kebab_case}}_api; @@ -545,7 +545,7 @@ server { proxy_send_timeout 30s; proxy_read_timeout 30s; } - + # Health check endpoint location /health { proxy_pass http://{{kebab_case}}_api; @@ -556,7 +556,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; access_log off; } - + # Root endpoint location / { proxy_pass http://{{kebab_case}}_api; @@ -573,8 +573,8 @@ server { "#; } - // New structured logging module - pub const LOGGING_PY: &str = r#"from __future__ import annotations +// New structured logging module +pub const LOGGING_PY: &str = r#"from __future__ import annotations import logging import logging.config @@ -589,7 +589,7 @@ from app.core.config import settings def setup_logging() -> None: """Configure structured logging with structlog""" - + # Configure structlog if settings.LOG_FORMAT == "json": processors = [ @@ -614,7 +614,7 @@ def setup_logging() -> None: structlog.processors.format_exc_info, structlog.dev.ConsoleRenderer() ] - + structlog.configure( processors=processors, context_class=dict, @@ -622,7 +622,7 @@ def setup_logging() -> None: wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) - + # Configure standard library logging logging.basicConfig( format="%(message)s", @@ -632,14 +632,14 @@ def setup_logging() -> None: class RequestIdMiddleware(BaseHTTPMiddleware): """Middleware to add request ID to each request""" - + async def dispatch(self, request: Request, call_next) -> Response: # Generate or extract request ID request_id = request.headers.get("X-Request-ID") or str(uuid.uuid4()) - + # Store request ID in request state request.state.request_id = request_id - + # Set up structured logging context logger = structlog.get_logger() logger = logger.bind( @@ -649,28 +649,28 @@ class RequestIdMiddleware(BaseHTTPMiddleware): user_agent=request.headers.get("User-Agent", ""), remote_addr=request.client.host if request.client else None ) - + # Log request logger.info( "Request started", query_params=dict(request.query_params) ) - + try: response = await call_next(request) - + # Log response logger.info( "Request completed", status_code=response.status_code, response_time_ms=None # Could add timing here ) - + # Add request ID to response headers response.headers["X-Request-ID"] = request_id - + return response - + except Exception as exc: logger.error( "Request failed", @@ -679,9 +679,9 @@ class RequestIdMiddleware(BaseHTTPMiddleware): ) raise "#; - - // Rate limiting middleware - pub const RATE_LIMITING_PY: &str = r#"from __future__ import annotations + +// Rate limiting middleware +pub const RATE_LIMITING_PY: &str = r#"from __future__ import annotations import time from typing import Optional @@ -698,39 +698,39 @@ logger = structlog.get_logger() class InMemoryRateLimiter: """Simple in-memory rate limiter using sliding window""" - + def __init__(self): self.requests = defaultdict(deque) self.lock = Lock() - + def is_allowed(self, key: str, limit: int, window: int) -> bool: """Check if request is allowed under rate limit""" now = time.time() - + with self.lock: # Remove old requests outside the window while self.requests[key] and self.requests[key][0] <= now - window: self.requests[key].popleft() - + # Check if under limit if len(self.requests[key]) >= limit: return False - + # Add current request self.requests[key].append(now) return True - + def get_remaining(self, key: str, limit: int, window: int) -> int: """Get remaining requests allowed""" now = time.time() - + with self.lock: # Clean old requests while self.requests[key] and self.requests[key][0] <= now - window: self.requests[key].popleft() - + return max(0, limit - len(self.requests[key])) - + def get_reset_time(self, key: str, window: int) -> Optional[float]: """Get time when rate limit resets""" with self.lock: @@ -740,33 +740,33 @@ class InMemoryRateLimiter: class RateLimitMiddleware(BaseHTTPMiddleware): """Rate limiting middleware""" - + def __init__(self, app, calls: int = 100, period: int = 60): super().__init__(app) self.calls = calls self.period = period self.limiter = InMemoryRateLimiter() - + def get_client_key(self, request: Request) -> str: """Get client identifier for rate limiting""" # Use X-Forwarded-For if available (behind proxy) forwarded_for = request.headers.get("X-Forwarded-For") if forwarded_for: return forwarded_for.split(",")[0].strip() - + # Fall back to direct client IP return request.client.host if request.client else "unknown" - + async def dispatch(self, request: Request, call_next) -> Response: if not settings.RATE_LIMIT_ENABLED: return await call_next(request) - + # Skip rate limiting for health checks if request.url.path in ["/health", "/health/ready", "/health/live"]: return await call_next(request) - + client_key = self.get_client_key(request) - + # Check rate limit if not self.limiter.is_allowed(client_key, self.calls, self.period): logger.warning( @@ -775,9 +775,9 @@ class RateLimitMiddleware(BaseHTTPMiddleware): path=request.url.path, method=request.method ) - + reset_time = self.limiter.get_reset_time(client_key, self.period) - + raise HTTPException( status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Rate limit exceeded", @@ -788,18 +788,18 @@ class RateLimitMiddleware(BaseHTTPMiddleware): "Retry-After": str(self.period) } ) - + # Add rate limit headers to response response = await call_next(request) - + remaining = self.limiter.get_remaining(client_key, self.calls, self.period) reset_time = self.limiter.get_reset_time(client_key, self.period) - + response.headers["X-RateLimit-Limit"] = str(self.calls) response.headers["X-RateLimit-Remaining"] = str(remaining) if reset_time: response.headers["X-RateLimit-Reset"] = str(int(reset_time)) - + return response "#; @@ -901,7 +901,7 @@ func main() { // Graceful shutdown with extended timeout ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second) defer cancel() - + if err := srv.Shutdown(ctx); err != nil { log.Error("Server forced shutdown", "error", err) os.Exit(1) @@ -918,25 +918,25 @@ go 1.22 require ( // Core framework github.com/gin-gonic/gin v1.10.0 - + // Authentication & Security github.com/golang-jwt/jwt/v5 v5.2.1 golang.org/x/crypto v0.28.0 - + // Configuration github.com/joho/godotenv v1.5.1 - + // Database drivers & ORM go.mongodb.org/mongo-driver v1.17.1 github.com/lib/pq v1.10.9 github.com/jmoiron/sqlx v1.4.0 - + // Utilities github.com/google/uuid v1.6.0 - + // Validation github.com/go-playground/validator/v10 v10.22.1 - + // Testing github.com/stretchr/testify v1.9.0 ) @@ -986,7 +986,7 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ # Run the application CMD ["./main"] "#; - + // New structured logging module for Go with slog pub const GO_LOGGER: &str = r#"package logger @@ -1115,7 +1115,7 @@ def create_app(config_class: Type[Config] = Config) -> Flask: def before_request() -> None: g.request_id = request.headers.get('X-Request-ID', str(uuid.uuid4())) g.start_time = time.time() - + logger.info( \"Request started\", request_id=g.request_id, @@ -1128,16 +1128,16 @@ def create_app(config_class: Type[Config] = Config) -> Flask: @app.after_request def after_request(response): response.headers['X-Request-ID'] = g.get('request_id', 'unknown') - + duration = time.time() - g.get('start_time', time.time()) - + logger.info( \"Request completed\", request_id=g.get('request_id'), status_code=response.status_code, duration_ms=round(duration * 1000, 2) ) - + return response # Error handlers with structured logging @@ -1167,7 +1167,7 @@ def create_app(config_class: Type[Config] = Config) -> Flask: @app.route('/') def index(): return { - 'message': '{{project_name}} API is running', + 'message': '{{project_name}} API is running', 'version': '1.0.0', 'environment': app.config.get('FLASK_ENV', 'unknown'), 'request_id': g.get('request_id') @@ -1185,17 +1185,17 @@ from typing import Dict, Type class Config: \"\"\"Base configuration class with 2025 best practices\"\"\" - + # Basic Flask configuration SECRET_KEY: str = os.environ.get('SECRET_KEY', '{{secret_key}}') - + # Application settings PROJECT_NAME: str = '{{project_name}}' VERSION: str = '1.0.0' - + # Database configuration with connection pooling SQLALCHEMY_DATABASE_URI: str = os.environ.get( - 'DATABASE_URL', + 'DATABASE_URL', 'postgresql://user:password@localhost/{{snake_case}}_db' ) SQLALCHEMY_TRACK_MODIFICATIONS: bool = False @@ -1205,7 +1205,7 @@ class Config: 'pool_recycle': int(os.environ.get('DB_POOL_RECYCLE', '3600')), 'max_overflow': int(os.environ.get('DB_MAX_OVERFLOW', '0')) } - + # JWT Configuration with enhanced security JWT_SECRET_KEY: str = os.environ.get('JWT_SECRET_KEY', '{{secret_key}}') JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta( @@ -1217,23 +1217,23 @@ class Config: JWT_ALGORITHM: str = 'HS256' JWT_BLACKLIST_ENABLED: bool = True JWT_BLACKLIST_TOKEN_CHECKS: list = ['access', 'refresh'] - + # CORS with enhanced security CORS_ORIGINS: list = os.environ.get( - 'ALLOWED_ORIGINS', + 'ALLOWED_ORIGINS', 'http://localhost:3000,http://127.0.0.1:3000' ).split(',') CORS_METHODS: list = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'] CORS_ALLOW_HEADERS: list = ['Content-Type', 'Authorization'] - + # Rate limiting RATELIMIT_STORAGE_URL: str = os.environ.get('REDIS_URL', 'redis://localhost:6379') RATELIMIT_DEFAULT: str = \"200 per day, 50 per hour\" - + # Logging configuration LOG_LEVEL: str = os.environ.get('LOG_LEVEL', 'INFO') LOG_FORMAT: str = os.environ.get('LOG_FORMAT', 'json') - + # Security headers SECURITY_HEADERS: Dict = { 'X-Frame-Options': 'DENY', @@ -1242,20 +1242,20 @@ class Config: 'Referrer-Policy': 'strict-origin-when-cross-origin', 'Content-Security-Policy': \"default-src 'self'\" } - + # Environment FLASK_ENV: str = os.environ.get('FLASK_ENV', 'development') DEBUG: bool = os.environ.get('FLASK_DEBUG', '0') == '1' TESTING: bool = False - + @property def is_development(self) -> bool: return self.FLASK_ENV == 'development' - + @property def is_production(self) -> bool: return self.FLASK_ENV == 'production' - + @property def is_testing(self) -> bool: return self.TESTING @@ -1264,27 +1264,27 @@ class DevelopmentConfig(Config): \"\"\"Development configuration\"\"\" DEBUG: bool = True LOG_LEVEL: str = 'DEBUG' - + class ProductionConfig(Config): \"\"\"Production configuration with enhanced security\"\"\" DEBUG: bool = False TESTING: bool = False - + # Enhanced security for production SESSION_COOKIE_SECURE: bool = True SESSION_COOKIE_HTTPONLY: bool = True SESSION_COOKIE_SAMESITE: str = 'Lax' - + # Force HTTPS in production PREFERRED_URL_SCHEME: str = 'https' - + class TestingConfig(Config): \"\"\"Testing configuration\"\"\" TESTING: bool = True DEBUG: bool = True SQLALCHEMY_DATABASE_URI: str = 'sqlite:///:memory:' JWT_ACCESS_TOKEN_EXPIRES: timedelta = timedelta(minutes=5) - + # Disable rate limiting in tests RATELIMIT_ENABLED: bool = False @@ -1388,13 +1388,13 @@ def register(): data = schema.load(request.get_json()) except ValidationError as err: return jsonify({'errors': err.messages}), 400 - + user_service = UserService() - + # Check if user already exists if user_service.get_by_email(data['email']): return jsonify({'message': 'User already exists'}), 409 - + # Create new user hashed_password = hash_password(data['password']) user = user_service.create({ @@ -1402,10 +1402,10 @@ def register(): 'password_hash': hashed_password, 'full_name': data['full_name'] }) - + # Generate tokens tokens = generate_tokens(str(user.id)) - + logger.info(f"New user registered: {user.email}") return jsonify(tokens), 201 @@ -1417,20 +1417,20 @@ def login(): data = schema.load(request.get_json()) except ValidationError as err: return jsonify({'errors': err.messages}), 400 - + user_service = UserService() user = user_service.get_by_email(data['email']) - + if not user or not verify_password(data['password'], user.password_hash): logger.warning(f"Failed login attempt for email: {data['email']}") return jsonify({'message': 'Invalid credentials'}), 401 - + if not user.is_active: return jsonify({'message': 'Account is deactivated'}), 403 - + # Generate tokens tokens = generate_tokens(str(user.id)) - + logger.info(f"User logged in: {user.email}") return jsonify(tokens), 200 @@ -1438,16 +1438,16 @@ def login(): @jwt_required(refresh=True) def refresh(): current_user_id = get_jwt_identity() - + user_service = UserService() user = user_service.get_by_id(current_user_id) - + if not user or not user.is_active: return jsonify({'message': 'User not found or inactive'}), 404 - + # Create new access token new_access_token = create_access_token(identity=current_user_id) - + return jsonify({ 'access_token': new_access_token, 'token_type': 'bearer' @@ -1466,13 +1466,13 @@ bp = Blueprint('users', __name__) @jwt_required() def get_current_user(): current_user_id = get_jwt_identity() - + user_service = UserService() user = user_service.get_by_id(current_user_id) - + if not user: return jsonify({'message': 'User not found'}), 404 - + schema = UserResponseSchema() return jsonify(schema.dump(user)), 200 "#; @@ -1508,7 +1508,7 @@ from app.core.extensions import db class User(db.Model): __tablename__ = 'users' - + id = db.Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) email = db.Column(db.String(120), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) @@ -1516,10 +1516,10 @@ class User(db.Model): is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - + def __repr__(self): return f'' - + def to_dict(self): return { 'id': str(self.id), @@ -1537,7 +1537,7 @@ from app.core.extensions import db class User(db.Model): __tablename__ = 'users' - + id = db.Column(db.String(36), primary_key=True, default=lambda: str(uuid.uuid4())) email = db.Column(db.String(120), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) @@ -1545,10 +1545,10 @@ class User(db.Model): is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) - + def __repr__(self): return f'' - + def to_dict(self): return { 'id': self.id, @@ -1596,7 +1596,7 @@ class UserService: full_name=user_data['full_name'], is_active=user_data.get('is_active', True) ) - + db.session.add(user) db.session.commit() return user @@ -1614,11 +1614,11 @@ class UserService: user = self.get_by_id(user_id) if not user: return None - + for key, value in update_data.items(): if hasattr(user, key): setattr(user, key, value) - + db.session.commit() return user @@ -1627,19 +1627,19 @@ class UserService: user = self.get_by_id(user_id) if not user: return False - + db.session.delete(user) db.session.commit() return True - + def list_users(self, page: int = 1, per_page: int = 20) -> Dict[str, Any]: """List users with pagination""" users = User.query.paginate( - page=page, - per_page=per_page, + page=page, + per_page=per_page, error_out=False ) - + return { 'users': [user.to_dict() for user in users.items], 'total': users.total, @@ -1661,10 +1661,10 @@ def admin_required(f): current_user_id = get_jwt_identity() user_service = UserService() user = user_service.get_by_id(current_user_id) - + if not user or not getattr(user, 'is_admin', False): return jsonify({'message': 'Admin access required'}), 403 - + return f(*args, **kwargs) return decorated_function @@ -1675,10 +1675,10 @@ def active_user_required(f): current_user_id = get_jwt_identity() user_service = UserService() user = user_service.get_by_id(current_user_id) - + if not user or not user.is_active: return jsonify({'message': 'Account is inactive'}), 403 - + return f(*args, **kwargs) return decorated_function "#; @@ -2020,7 +2020,7 @@ events { http { include /etc/nginx/mime.types; default_type application/octet-stream; - + # Basic settings sendfile on; tcp_nopush on; @@ -2028,17 +2028,17 @@ http { keepalive_timeout 65; types_hash_max_size 2048; client_max_body_size 16M; - + # Security headers add_header X-Frame-Options DENY always; add_header X-Content-Type-Options nosniff always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always; - + # Rate limiting limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; - + # Gzip compression gzip on; gzip_vary on; @@ -2053,14 +2053,14 @@ http { application/javascript application/xml+rss application/atom+xml; - + # Logging format log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - + access_log /var/log/nginx/access.log main; - + # Include server configurations include /etc/nginx/conf.d/*.conf; } @@ -2075,10 +2075,10 @@ upstream {{kebab_case}}_api { server { listen 80; server_name _; - + # Apply rate limiting limit_req zone=api burst=20 nodelay; - + # API proxy location /api/ { proxy_pass http://{{kebab_case}}_api; @@ -2094,7 +2094,7 @@ server { proxy_send_timeout 30s; proxy_read_timeout 30s; } - + # Health check endpoint location /health { proxy_pass http://{{kebab_case}}_api; @@ -2105,7 +2105,7 @@ server { proxy_set_header X-Forwarded-Proto $scheme; access_log off; } - + # Root endpoint location / { proxy_pass http://{{kebab_case}}_api; @@ -2132,15 +2132,15 @@ from app.core.config import TestingConfig def app(): """Create application for the tests.""" db_fd, db_path = tempfile.mkstemp() - + app = create_app(TestingConfig) app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{db_path}' - + with app.app_context(): db.create_all() yield app db.drop_all() - + os.close(db_fd) os.unlink(db_path) @@ -2177,14 +2177,14 @@ import pytest def test_register_user(client): """Test user registration.""" - response = client.post('/api/v1/auth/register', + response = client.post('/api/v1/auth/register', data=json.dumps({ 'email': 'test@example.com', 'password': 'testpassword123', 'full_name': 'Test User' }), content_type='application/json') - + assert response.status_code == 201 data = response.get_json() assert 'access_token' in data @@ -2201,7 +2201,7 @@ def test_login_user(client): 'full_name': 'Login User' }), content_type='application/json') - + # Then login response = client.post('/api/v1/auth/login', data=json.dumps({ @@ -2209,7 +2209,7 @@ def test_login_user(client): 'password': 'testpassword123' }), content_type='application/json') - + assert response.status_code == 200 data = response.get_json() assert 'access_token' in data @@ -2223,7 +2223,7 @@ def test_invalid_login(client): 'password': 'wrongpassword' }), content_type='application/json') - + assert response.status_code == 401 data = response.get_json() assert data['message'] == 'Invalid credentials' @@ -2235,22 +2235,22 @@ def test_duplicate_registration(client): 'password': 'testpassword123', 'full_name': 'Duplicate User' } - + # Register first time client.post('/api/v1/auth/register', data=json.dumps(user_data), content_type='application/json') - + # Try to register again response = client.post('/api/v1/auth/register', data=json.dumps(user_data), content_type='application/json') - + assert response.status_code == 409 data = response.get_json() assert data['message'] == 'User already exists' "#; - + // New structured logging module for Flask pub const FLASK_LOGGING_PY: &str = r#"from __future__ import annotations @@ -2262,10 +2262,10 @@ from flask import Flask, has_request_context, g def setup_logging(app: Flask) -> None: """Configure structured logging for Flask with structlog""" - + log_level = app.config.get('LOG_LEVEL', 'INFO') log_format = app.config.get('LOG_FORMAT', 'json') - + # Configure structlog if log_format == 'json': processors = [ @@ -2292,7 +2292,7 @@ def setup_logging(app: Flask) -> None: add_flask_context, structlog.dev.ConsoleRenderer() ] - + structlog.configure( processors=processors, context_class=dict, @@ -2300,14 +2300,14 @@ def setup_logging(app: Flask) -> None: wrapper_class=structlog.stdlib.BoundLogger, cache_logger_on_first_use=True, ) - + # Configure standard library logging logging.basicConfig( format='%(message)s', level=getattr(logging, log_level.upper()), force=True, ) - + # Disable werkzeug logs in production if not app.debug: logging.getLogger('werkzeug').setLevel(logging.WARNING) @@ -2318,4 +2318,3345 @@ def add_flask_context(logger, method_name: str, event_dict: Dict[str, Any]) -> D event_dict['request_id'] = getattr(g, 'request_id', None) return event_dict "#; -} \ No newline at end of file +} + +pub mod php { + // Laravel Templates with Clean Architecture + pub const LARAVEL_COMPOSER_JSON: &str = r#"{ + "name": "{{kebab_case}}/{{kebab_case}}", + "type": "project", + "description": "{{project_name}} - Laravel application with Clean Architecture", + "keywords": ["laravel", "clean-architecture", "ddd", "api"], + "license": "MIT", + "require": { + "php": "^8.2", + "laravel/framework": "^11.0", + "laravel/sanctum": "^4.0", + "tymon/jwt-auth": "^2.0", + "guzzlehttp/guzzle": "^7.2", + "spatie/laravel-data": "^4.0", + "spatie/laravel-query-builder": "^5.0" + }, + "require-dev": { + "fakerphp/faker": "^1.23", + "laravel/pint": "^1.0", + "laravel/sail": "^1.18", + "mockery/mockery": "^1.4.4", + "nunomaduro/collision": "^8.0", + "phpunit/phpunit": "^10.0", + "spatie/laravel-ignition": "^2.0", + "larastan/larastan": "^2.0" + }, + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "scripts": { + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" + ], + "post-update-cmd": [ + "@php artisan vendor:publish --tag=laravel-assets --ansi --force" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate --ansi" + ], + "test": "phpunit", + "test-coverage": "phpunit --coverage-html coverage", + "pint": "pint", + "stan": "phpstan analyse" + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "config": { + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + } + }, + "minimum-stability": "stable", + "prefer-stable": true +} +"#; + + pub const LARAVEL_ARTISAN: &str = r#"#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +$kernel->terminate($input, $status); + +exit($status); +"#; + + pub const LARAVEL_APP_CONFIG: &str = r#" env('APP_NAME', '{{project_name}}'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + */ + + 'locale' => 'en', + + 'fallback_locale' => 'en', + + 'faker_locale' => 'en_US', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + */ + + 'maintenance' => [ + 'driver' => 'file', + ], + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + */ + + 'providers' => ServiceProvider::defaultProviders()->merge([ + /* + * Package Service Providers... + */ + Tymon\JWTAuth\Providers\LaravelServiceProvider::class, + + /* + * Application Service Providers... + */ + App\Infrastructure\Providers\AppServiceProvider::class, + App\Infrastructure\Providers\AuthServiceProvider::class, + App\Infrastructure\Providers\EventServiceProvider::class, + App\Infrastructure\Providers\RouteServiceProvider::class, + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + */ + + 'aliases' => Facade::defaultAliases()->merge([ + 'JWTAuth' => Tymon\JWTAuth\Facades\JWTAuth::class, + 'JWTFactory' => Tymon\JWTAuth\Facades\JWTFactory::class, + ])->toArray(), + +]; +"#; + + pub const LARAVEL_DATABASE_CONFIG: &str = r#" env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', '{{snake_case}}_db'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', '{{snake_case}}_db'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + ], + + ], + +]; +"#; + + pub const LARAVEL_AUTH_CONFIG: &str = r#" [ + 'guard' => 'api', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + 'api' => [ + 'driver' => 'jwt', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Domain\User\Entities\User::class, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => 'password_reset_tokens', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + */ + + 'password_timeout' => 10800, + +]; +"#; + + // User Entity (Domain Layer) + pub const LARAVEL_USER_ENTITY: &str = r#" + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'email_verified_at', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var array + */ + protected $hidden = [ + 'password', + 'remember_token', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + ]; + } + + /** + * Get the identifier that will be stored in the subject claim of the JWT. + */ + public function getJWTIdentifier() + { + return $this->getKey(); + } + + /** + * Return a key value array, containing any custom claims to be added to the JWT. + */ + public function getJWTCustomClaims(): array + { + return []; + } + + /** + * Business logic methods + */ + public function isEmailVerified(): bool + { + return !is_null($this->email_verified_at); + } + + public function markEmailAsVerified(): void + { + if (is_null($this->email_verified_at)) { + $this->email_verified_at = now(); + $this->save(); + } + } + + public function getDisplayName(): string + { + return $this->name ?? $this->email; + } +} +"#; + + // User Repository Interface (Domain Layer) + pub const LARAVEL_USER_REPOSITORY: &str = r#"userRepository->existsByEmail($userData['email'])) { + throw ValidationException::withMessages([ + 'email' => ['A user with this email already exists.'] + ]); + } + + $userData['password'] = Hash::make($userData['password']); + + return $this->userRepository->create($userData); + } + + public function updateUser(User $user, array $userData): User + { + if (isset($userData['email']) && + $userData['email'] !== $user->email && + $this->userRepository->existsByEmail($userData['email'])) { + throw ValidationException::withMessages([ + 'email' => ['A user with this email already exists.'] + ]); + } + + if (isset($userData['password'])) { + $userData['password'] = Hash::make($userData['password']); + } + + return $this->userRepository->update($user, $userData); + } + + public function getUserById(int $id): ?User + { + return $this->userRepository->findById($id); + } + + public function getUserByEmail(string $email): ?User + { + return $this->userRepository->findByEmail($email); + } + + public function deleteUser(User $user): bool + { + return $this->userRepository->delete($user); + } + + public function getPaginatedUsers(int $perPage = 15) + { + return $this->userRepository->paginate($perPage); + } +} +"#; + + // Auth Service (Domain Layer) + pub const LARAVEL_AUTH_SERVICE: &str = r#"userRepository->findByEmail($email); + + if (!$user || !Hash::check($password, $user->password)) { + throw ValidationException::withMessages([ + 'email' => ['The provided credentials are incorrect.'] + ]); + } + + $token = JWTAuth::fromUser($user); + + return [ + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => config('jwt.ttl') * 60, + 'user' => $user->only(['id', 'name', 'email', 'email_verified_at']) + ]; + } + + public function register(array $userData): array + { + if ($this->userRepository->existsByEmail($userData['email'])) { + throw ValidationException::withMessages([ + 'email' => ['A user with this email already exists.'] + ]); + } + + $userData['password'] = Hash::make($userData['password']); + $user = $this->userRepository->create($userData); + + $token = JWTAuth::fromUser($user); + + return [ + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => config('jwt.ttl') * 60, + 'user' => $user->only(['id', 'name', 'email', 'email_verified_at']) + ]; + } + + public function logout(): void + { + JWTAuth::invalidate(JWTAuth::getToken()); + } + + public function refresh(): string + { + return JWTAuth::refresh(JWTAuth::getToken()); + } + + public function me(): User + { + return JWTAuth::parseToken()->authenticate(); + } +} +"#; + + // Create User Command (Application Layer) + pub const LARAVEL_CREATE_USER_COMMAND: &str = r#" $this->name, + 'email' => $this->email, + 'password' => $this->password, + 'email_verified_at' => $this->emailVerifiedAt, + ]; + } +} +"#; + + // Get User Query (Application Layer) + pub const LARAVEL_GET_USER_QUERY: &str = r#"userService->createUser($command->toArray()); + } + + public function handleGetUser(GetUserQuery $query): ?User + { + return $this->userService->getUserById($query->userId); + } +} +"#; + + // Login Command (Application Layer) + pub const LARAVEL_LOGIN_COMMAND: &str = r#"authService->login($command->email, $command->password); + } + + public function handleRegister(CreateUserCommand $command): array + { + return $this->authService->register($command->toArray()); + } + + public function handleLogout(): void + { + $this->authService->logout(); + } + + public function handleRefresh(): string + { + return $this->authService->refresh(); + } + + public function handleMe() + { + return $this->authService->me(); + } +} +"#; + + // Eloquent User Repository (Infrastructure Layer) + pub const LARAVEL_ELOQUENT_USER_REPOSITORY: &str = r#"first(); + } + + public function create(array $data): User + { + return User::create($data); + } + + public function update(User $user, array $data): User + { + $user->update($data); + return $user->fresh(); + } + + public function delete(User $user): bool + { + return $user->delete(); + } + + public function paginate(int $perPage = 15): LengthAwarePaginator + { + return User::paginate($perPage); + } + + public function existsByEmail(string $email): bool + { + return User::where('email', $email)->exists(); + } +} +"#; + + // User Controller (Infrastructure Layer) + pub const LARAVEL_USER_CONTROLLER: &str = r#"json(['message' => 'Users list']); + } + + public function show(int $id): JsonResponse + { + $query = new GetUserQuery($id); + $user = $this->userHandler->handleGetUser($query); + + if (!$user) { + return response()->json(['message' => 'User not found'], 404); + } + + return response()->json([ + 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) + ]); + } + + public function store(CreateUserRequest $request): JsonResponse + { + $command = new CreateUserCommand( + name: $request->validated('name'), + email: $request->validated('email'), + password: $request->validated('password') + ); + + $user = $this->userHandler->handleCreateUser($command); + + return response()->json([ + 'message' => 'User created successfully', + 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) + ], 201); + } +} +"#; + + // Auth Controller (Infrastructure Layer) + pub const LARAVEL_AUTH_CONTROLLER: &str = r#"validated('name'), + email: $request->validated('email'), + password: $request->validated('password') + ); + + $result = $this->authHandler->handleRegister($command); + + return response()->json([ + 'message' => 'User registered successfully', + 'data' => $result + ], 201); + } catch (ValidationException $e) { + return response()->json([ + 'message' => 'Validation failed', + 'errors' => $e->errors() + ], 422); + } + } + + public function login(LoginRequest $request): JsonResponse + { + try { + $command = new LoginCommand( + email: $request->validated('email'), + password: $request->validated('password') + ); + + $result = $this->authHandler->handleLogin($command); + + return response()->json([ + 'message' => 'Login successful', + 'data' => $result + ]); + } catch (ValidationException $e) { + return response()->json([ + 'message' => 'Invalid credentials', + 'errors' => $e->errors() + ], 401); + } + } + + public function logout(): JsonResponse + { + $this->authHandler->handleLogout(); + + return response()->json([ + 'message' => 'Successfully logged out' + ]); + } + + public function refresh(): JsonResponse + { + $token = $this->authHandler->handleRefresh(); + + return response()->json([ + 'access_token' => $token, + 'token_type' => 'bearer', + 'expires_in' => config('jwt.ttl') * 60 + ]); + } + + public function me(): JsonResponse + { + $user = $this->authHandler->handleMe(); + + return response()->json([ + 'data' => $user->only(['id', 'name', 'email', 'email_verified_at']) + ]); + } +} +"#; + + // Register Request (Infrastructure Layer) + pub const LARAVEL_REGISTER_REQUEST: &str = r#" ['required', 'string', 'max:255'], + 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'password' => ['required', 'string', 'min:8', 'confirmed'], + ]; + } + + public function messages(): array + { + return [ + 'name.required' => 'Name is required', + 'email.required' => 'Email is required', + 'email.email' => 'Email must be a valid email address', + 'email.unique' => 'This email is already registered', + 'password.required' => 'Password is required', + 'password.min' => 'Password must be at least 8 characters', + 'password.confirmed' => 'Password confirmation does not match', + ]; + } +} +"#; + + // Login Request (Infrastructure Layer) + pub const LARAVEL_LOGIN_REQUEST: &str = r#" ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + } + + public function messages(): array + { + return [ + 'email.required' => 'Email is required', + 'email.email' => 'Email must be a valid email address', + 'password.required' => 'Password is required', + ]; + } +} +"#; + + // API Routes + pub const LARAVEL_API_ROUTES: &str = r#"json([ + 'status' => 'healthy', + 'service' => '{{project_name}} API', + 'version' => '1.0.0', + 'timestamp' => now()->toISOString() + ]); +}); + +Route::prefix('v1')->group(function () { + // Authentication routes + Route::prefix('auth')->group(function () { + Route::post('register', [AuthController::class, 'register']); + Route::post('login', [AuthController::class, 'login']); + + Route::middleware('auth:api')->group(function () { + Route::post('logout', [AuthController::class, 'logout']); + Route::post('refresh', [AuthController::class, 'refresh']); + Route::get('me', [AuthController::class, 'me']); + }); + }); + + // Protected routes + Route::middleware('auth:api')->group(function () { + Route::apiResource('users', UserController::class); + }); +}); +"#; + + // App Service Provider + pub const LARAVEL_APP_SERVICE_PROVIDER: &str = r#"app->bind(UserRepositoryInterface::class, EloquentUserRepository::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + // + } +} +"#; + + // Multi-stage Dockerfile for PHP + #[allow(dead_code)] + pub const PHP_DOCKERFILE: &str = r#"# ========================= +# Build stage +# ========================= +FROM composer:2.6 AS composer + +COPY composer.json composer.lock ./ +RUN composer install --no-dev --no-scripts --optimize-autoloader --no-interaction + +# ========================= +# Runtime stage +# ========================= +FROM php:8.2-fpm-alpine AS runtime + +# Install system dependencies +RUN apk add --no-cache \ + nginx \ + postgresql-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + zip \ + libzip-dev \ + icu-dev \ + oniguruma-dev \ + curl \ + git \ + supervisor + +# Install PHP extensions +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + gd \ + pdo \ + pdo_pgsql \ + pdo_mysql \ + zip \ + intl \ + mbstring \ + opcache \ + bcmath + +# Install Redis extension +RUN apk add --no-cache $PHPIZE_DEPS \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && apk del $PHPIZE_DEPS + +# Create application user +RUN addgroup -g 1000 -S www && \ + adduser -u 1000 -S www -G www + +# Set working directory +WORKDIR /var/www/html + +# Copy composer dependencies +COPY --from=composer --chown=www:www /app/vendor ./vendor + +# Copy application code +COPY --chown=www:www . . + +# Copy configuration files +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf +COPY docker/nginx/default.conf /etc/nginx/http.d/default.conf +COPY docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/www.conf +COPY docker/php/php.ini /usr/local/etc/php/php.ini + +# Create necessary directories and set permissions +RUN mkdir -p /var/www/html/storage/logs \ + /var/www/html/storage/framework/cache \ + /var/www/html/storage/framework/sessions \ + /var/www/html/storage/framework/views \ + /var/www/html/bootstrap/cache \ + /run/nginx \ + /var/log/supervisor \ + && chown -R www:www /var/www/html/storage \ + && chown -R www:www /var/www/html/bootstrap/cache \ + && chmod -R 775 /var/www/html/storage \ + && chmod -R 775 /var/www/html/bootstrap/cache + +# Copy supervisor configuration +COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# Switch to www user +USER www + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost/health || exit 1 + +# Start supervisor +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +"#; + + // Laravel Docker Compose + pub const LARAVEL_DOCKER_COMPOSE: &str = r#"services: + app: + build: + context: . + dockerfile: docker/php/Dockerfile + target: production + container_name: {{kebab_case}}-app + env_file: + - .env.docker + environment: + - APP_NAME={{project_name}} + - DB_CONNECTION=pgsql + - DB_HOST=postgres + - DB_PORT=5432 + - DB_DATABASE={{snake_case}}_db + - REDIS_HOST=redis + - REDIS_PORT=6379 + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + volumes: + - app_data:/var/www/html + - app_logs:/var/www/html/storage/logs + networks: + - {{kebab_case}}-network + restart: unless-stopped + healthcheck: + test: ["CMD", "php", "artisan", "app:health-check"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + nginx: + build: + context: . + dockerfile: docker/nginx/Dockerfile + container_name: {{kebab_case}}-nginx + ports: + - "80:80" + - "443:443" + volumes: + - app_data:/var/www/html:ro + - nginx_logs:/var/log/nginx + depends_on: + app: + condition: service_healthy + networks: + - {{kebab_case}}-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/api/health"] + interval: 30s + timeout: 10s + retries: 3 + + postgres: + image: postgres:16-alpine + container_name: {{kebab_case}}-postgres + env_file: + - .env.docker + environment: + - POSTGRES_DB={{snake_case}}_db + - POSTGRES_USER=postgres + volumes: + - postgres_data:/var/lib/postgresql/data + expose: + - "5432" + networks: + - {{kebab_case}}-network + restart: unless-stopped + healthcheck: + test: ["CMD", "pg_isready", "-U", "postgres", "-d", "{{snake_case}}_db"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 30s + + redis: + image: redis:7-alpine + container_name: {{kebab_case}}-redis + volumes: + - redis_data:/data + - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro + expose: + - "6379" + networks: + - {{kebab_case}}-network + restart: unless-stopped + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 30s + timeout: 10s + retries: 3 + command: redis-server /usr/local/etc/redis/redis.conf + +volumes: + postgres_data: + redis_data: + app_data: + app_logs: + nginx_logs: + +networks: + {{kebab_case}}-network: + driver: bridge +"#; + + // Nginx Configuration + pub const PHP_NGINX_CONF: &str = r#"user www; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + # Logging + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + # Basic Settings + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + client_max_body_size 16M; + + # Gzip Settings + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_types + text/plain + text/css + text/xml + text/javascript + application/json + application/javascript + application/xml+rss + application/atom+xml; + + # Security Headers + add_header X-Frame-Options DENY always; + add_header X-Content-Type-Options nosniff always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Include server configurations + include /etc/nginx/http.d/*.conf; +} +"#; + + // Laravel Nginx Default Configuration + pub const LARAVEL_NGINX_DEFAULT_CONF: &str = r#"server { + listen 80; + server_name _; + root /var/www/html/public; + index index.php index.html; + + # Security + server_tokens off; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Laravel-specific configuration + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # Handle PHP files + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # FastCGI settings + fastcgi_connect_timeout 60; + fastcgi_send_timeout 180; + fastcgi_read_timeout 180; + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + fastcgi_temp_file_write_size 256k; + fastcgi_intercept_errors on; + } + + # Deny access to sensitive files + location ~ /\.(?!well-known).* { + deny all; + } + + # Static files caching + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } +} +"#; + + // PHP-FPM Configuration + pub const PHP_FPM_CONF: &str = r#"[www] +user = www +group = www + +listen = 127.0.0.1:9000 +listen.owner = www +listen.group = www +listen.mode = 0660 + +pm = dynamic +pm.max_children = 20 +pm.start_servers = 2 +pm.min_spare_servers = 1 +pm.max_spare_servers = 3 +pm.max_requests = 1000 + +; Logging +access.log = /var/log/php-fpm.access.log +access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%" + +; Environment variables +env[PATH] = /usr/local/bin:/usr/bin:/bin +env[TMP] = /tmp +env[TMPDIR] = /tmp +env[TEMP] = /tmp + +; PHP admin values +php_admin_value[error_log] = /var/log/php-fpm.error.log +php_admin_flag[log_errors] = on +php_admin_value[memory_limit] = 256M +php_admin_value[upload_max_filesize] = 16M +php_admin_value[post_max_size] = 16M +php_admin_value[max_execution_time] = 120 +php_admin_value[max_input_time] = 120 +"#; + + // Laravel Environment Example + pub const LARAVEL_ENV_EXAMPLE: &str = r#"APP_NAME={{project_name}} +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE={{snake_case}}_db +DB_USERNAME=postgres +DB_PASSWORD= + +BROADCAST_DRIVER=log +CACHE_DRIVER=redis +FILESYSTEM_DISK=local +QUEUE_CONNECTION=sync +SESSION_DRIVER=redis +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +JWT_SECRET= +JWT_TTL=60 +JWT_REFRESH_TTL=20160 + +VITE_APP_NAME="${APP_NAME}" +"#; + + // Laravel PHPUnit Configuration + pub const LARAVEL_PHPUNIT_XML: &str = r#" + + + + tests/Unit + + + tests/Feature + + + tests/Integration + + + + + app + + + + + + + + + + + + + + +"#; + + // Laravel Auth Feature Test + pub const LARAVEL_AUTH_FEATURE_TEST: &str = r#" 'John Doe', + 'email' => 'john@example.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + ]; + + $response = $this->postJson('/api/v1/auth/register', $userData); + + $response->assertStatus(201) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'access_token', + 'token_type', + 'expires_in', + 'user' => [ + 'id', + 'name', + 'email', + ] + ] + ]); + + $this->assertDatabaseHas('users', [ + 'email' => 'john@example.com', + 'name' => 'John Doe', + ]); + } + + public function test_user_can_login_with_valid_credentials(): void + { + $user = User::factory()->create([ + 'email' => 'john@example.com', + 'password' => bcrypt('password123'), + ]); + + $loginData = [ + 'email' => 'john@example.com', + 'password' => 'password123', + ]; + + $response = $this->postJson('/api/v1/auth/login', $loginData); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'message', + 'data' => [ + 'access_token', + 'token_type', + 'expires_in', + 'user' + ] + ]); + } + + public function test_user_cannot_login_with_invalid_credentials(): void + { + $user = User::factory()->create([ + 'email' => 'john@example.com', + 'password' => bcrypt('password123'), + ]); + + $loginData = [ + 'email' => 'john@example.com', + 'password' => 'wrongpassword', + ]; + + $response = $this->postJson('/api/v1/auth/login', $loginData); + + $response->assertStatus(401) + ->assertJson([ + 'message' => 'Invalid credentials', + ]); + } + + public function test_authenticated_user_can_get_profile(): void + { + $user = User::factory()->create(); + $token = auth('api')->login($user); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->getJson('/api/v1/auth/me'); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'name', + 'email', + ] + ]); + } + + public function test_authenticated_user_can_logout(): void + { + $user = User::factory()->create(); + $token = auth('api')->login($user); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer ' . $token, + ])->postJson('/api/v1/auth/logout'); + + $response->assertStatus(200) + ->assertJson([ + 'message' => 'Successfully logged out', + ]); + } +} +"#; + + // Laravel User Unit Test + pub const LARAVEL_USER_UNIT_TEST: &str = r#" now(), + ]); + + $userWithoutVerifiedEmail = new User([ + 'email_verified_at' => null, + ]); + + $this->assertTrue($userWithVerifiedEmail->isEmailVerified()); + $this->assertFalse($userWithoutVerifiedEmail->isEmailVerified()); + } + + public function test_user_can_get_display_name(): void + { + $userWithName = new User([ + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + $userWithoutName = new User([ + 'name' => null, + 'email' => 'jane@example.com', + ]); + + $this->assertEquals('John Doe', $userWithName->getDisplayName()); + $this->assertEquals('jane@example.com', $userWithoutName->getDisplayName()); + } + + public function test_user_can_mark_email_as_verified(): void + { + $user = new User([ + 'email_verified_at' => null, + ]); + + $this->assertFalse($user->isEmailVerified()); + + $user->markEmailAsVerified(); + + $this->assertTrue($user->isEmailVerified()); + $this->assertNotNull($user->email_verified_at); + } +} +"#; + + // Laravel README + pub const LARAVEL_README: &str = r#"# {{project_name}} + +Production-ready Laravel application with Clean Architecture, Domain-Driven Design (DDD), and comprehensive JWT authentication. + +## Architecture + +This project follows **Clean Architecture** principles with clear separation of concerns: + +### Directory Structure + +``` +app/ +├── Domain/ # Business logic and entities +│ ├── User/ +│ │ ├── Entities/ # Domain entities (User.php) +│ │ ├── Repositories/ # Repository interfaces +│ │ └── Services/ # Domain services +│ └── Auth/ +│ └── Services/ # Authentication domain services +├── Application/ # Use cases and application logic +│ ├── User/ +│ │ ├── Commands/ # Command objects +│ │ ├── Queries/ # Query objects +│ │ └── Handlers/ # Command/Query handlers +│ └── Auth/ +│ ├── Commands/ +│ └── Handlers/ +└── Infrastructure/ # External concerns + ├── Http/ + │ ├── Controllers/ # API controllers + │ └── Requests/ # Form requests + └── Persistence/ + └── Eloquent/ # Repository implementations +``` + +## Features + +- **Clean Architecture** with Domain-Driven Design +- **JWT Authentication** with access tokens +- **Repository Pattern** for data access abstraction +- **Command/Query Separation** (CQRS-lite) +- **Docker** containerization with Nginx + PHP-FPM +- **PostgreSQL** database with migrations +- **Redis** for caching and sessions +- **Comprehensive Testing** (Unit, Feature, Integration) +- **Code Quality** tools (PHPStan, Pint) + +## Quick Start + +### With Docker (Recommended) + +```bash +# Clone and setup +git clone +cd {{kebab_case}} + +# Environment setup +cp .env.example .env +# Edit .env with your configuration + +# Build and start containers +docker-compose up --build -d + +# Install dependencies +docker-compose exec app composer install + +# Generate application key +docker-compose exec app php artisan key:generate + +# Generate JWT secret +docker-compose exec app php artisan jwt:secret + +# Run migrations +docker-compose exec app php artisan migrate + +# The API will be available at http://localhost:8000 +``` + +### Local Development + +```bash +# Install dependencies +composer install + +# Environment setup +cp .env.example .env +php artisan key:generate +php artisan jwt:secret + +# Database setup +php artisan migrate + +# Start development server +php artisan serve +``` + +## API Documentation + +### Authentication Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/v1/auth/register` | User registration | +| POST | `/api/v1/auth/login` | User login | +| POST | `/api/v1/auth/logout` | User logout | +| POST | `/api/v1/auth/refresh` | Refresh token | +| GET | `/api/v1/auth/me` | Get current user | + +### User Endpoints (Protected) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/v1/users` | List users | +| GET | `/api/v1/users/{id}` | Get user by ID | +| POST | `/api/v1/users` | Create user | + +### System Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/health` | Health check | + +## Testing + +```bash +# Run all tests +composer test + +# Run with coverage +composer test-coverage + +# Run specific test suite +./vendor/bin/phpunit --testsuite=Unit +./vendor/bin/phpunit --testsuite=Feature +./vendor/bin/phpunit --testsuite=Integration +``` + +## Code Quality + +```bash +# Fix code style +composer pint + +# Run static analysis +composer stan + +# Run all quality checks +composer pint && composer stan && composer test +``` + +## Docker Services + +- **app**: PHP 8.2-FPM with Laravel application +- **nginx**: Nginx web server (reverse proxy) +- **postgres**: PostgreSQL 16 database +- **redis**: Redis for caching and sessions + +## Security Features + +- JWT token-based authentication +- Password hashing with bcrypt +- CORS configuration +- Security headers (X-Frame-Options, CSP, etc.) +- Input validation and sanitization +- SQL injection prevention with Eloquent ORM + +## Environment Variables + +Key environment variables to configure: + +```env +APP_NAME={{project_name}} +APP_ENV=production +APP_KEY=base64:... +APP_URL=https://yourdomain.com + +DB_CONNECTION=pgsql +DB_HOST=postgres +DB_DATABASE={{snake_case}}_db +DB_USERNAME=postgres +DB_PASSWORD=your-secure-password + +JWT_SECRET=your-jwt-secret +JWT_TTL=60 + +REDIS_HOST=redis +REDIS_PORT=6379 +``` + +## Deployment + +1. Set up your production environment +2. Configure environment variables +3. Build Docker images +4. Deploy with docker-compose or Kubernetes +5. Run migrations: `php artisan migrate --force` + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Add tests for new features +4. Ensure code quality checks pass +5. Submit a pull request + +--- + +Generated by [Athena CLI](https://github.com/Jeck0v/Athena) +"#; + + // Symfony templates will be added here... + // For now, we'll add placeholder constants to avoid compilation errors + + pub const SYMFONY_COMPOSER_JSON: &str = r#"{ + "name": "{{kebab_case}}/{{kebab_case}}", + "type": "project", + "description": "{{project_name}} - Symfony application with Hexagonal Architecture", + "keywords": ["symfony", "hexagonal-architecture", "ddd", "api"], + "license": "MIT", + "require": { + "php": "^8.2", + "symfony/framework-bundle": "^7.0", + "symfony/security-bundle": "^7.0", + "symfony/console": "^7.0", + "symfony/dotenv": "^7.0", + "symfony/flex": "^2.0", + "symfony/runtime": "^7.0", + "symfony/yaml": "^7.0", + "symfony/maker-bundle": "^1.0", + "symfony/orm-pack": "^2.0", + "symfony/validator": "^7.0", + "symfony/serializer": "^7.0", + "symfony/property-access": "^7.0", + "symfony/property-info": "^7.0", + "lexik/jwt-authentication-bundle": "^3.0", + "doctrine/orm": "^3.0", + "doctrine/doctrine-bundle": "^2.0", + "doctrine/doctrine-migrations-bundle": "^3.0", + "gesdinet/jwt-refresh-token-bundle": "^1.0", + "nelmio/cors-bundle": "^2.0", + "ramsey/uuid": "^4.0", + "symfony/uid": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/phpunit-bridge": "^7.0", + "symfony/test-pack": "^1.0", + "phpstan/phpstan": "^1.0", + "friendsofphp/php-cs-fixer": "^3.0", + "doctrine/doctrine-fixtures-bundle": "^3.0" + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" + }, + "post-install-cmd": [ + "@auto-scripts" + ], + "post-update-cmd": [ + "@auto-scripts" + ], + "test": "phpunit", + "cs-fix": "php-cs-fixer fix", + "stan": "phpstan analyse" + }, + "extra": { + "symfony": { + "allow-contrib": false, + "require": "7.0.*" + } + }, + "config": { + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "symfony/runtime": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "minimum-stability": "stable", + "prefer-stable": true +} +"#; + + pub const SYMFONY_SERVICES_YAML: &str = r#"services: + _defaults: + autowire: true + autoconfigure: true + public: false + + App\: + resource: '../src/' + exclude: + - '../src/DependencyInjection/' + - '../src/Domain/*/Entities/' + - '../src/Domain/*/ValueObjects/' + - '../src/Kernel.php' + + # Domain Services + App\Domain\User\Repositories\UserRepositoryInterface: + alias: App\Infrastructure\Persistence\Doctrine\Repositories\DoctrineUserRepository + + # Application Services + App\Application\User\Services\: + resource: '../src/Application/User/Services/' + tags: ['app.application_service'] + + # Infrastructure Services + App\Infrastructure\: + resource: '../src/Infrastructure/' + exclude: + - '../src/Infrastructure/Persistence/Doctrine/Entities/' + + # Controllers + App\Infrastructure\Http\Controllers\: + resource: '../src/Infrastructure/Http/Controllers/' + tags: ['controller.service_arguments'] + + # Security + App\Infrastructure\Security\: + resource: '../src/Infrastructure/Security/' + + # Event Handlers + App\Application\User\EventHandlers\: + resource: '../src/Application/User/EventHandlers/' + tags: [kernel.event_listener] +"#; + + // Placeholder constants for Symfony (to be implemented) + pub const SYMFONY_DOCTRINE_CONFIG: &str = r#"doctrine: + dbal: + url: '%env(resolve:DATABASE_URL)%' + driver: 'pdo_{{database_driver}}' + server_version: '{{database_version}}' + charset: utf8mb4 + default_table_options: + charset: utf8mb4 + collate: utf8mb4_unicode_ci + + orm: + auto_generate_proxy_classes: true + enable_lazy_ghost_objects: true + naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware + auto_mapping: true + mappings: + App: + type: attribute + is_bundle: false + dir: '%kernel.project_dir%/src/Infrastructure/Persistence/Doctrine/Entities' + prefix: 'App\Infrastructure\Persistence\Doctrine\Entities' + alias: App + +when@prod: + doctrine: + orm: + auto_generate_proxy_classes: false + proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies' + query_cache_driver: + type: pool + pool: doctrine.query_cache_pool + result_cache_driver: + type: pool + pool: doctrine.result_cache_pool + + framework: + cache: + pools: + doctrine.query_cache_pool: + adapter: cache.app + doctrine.result_cache_pool: + adapter: cache.app +"#; + pub const SYMFONY_SECURITY_CONFIG: &str = r#"security: + password_hashers: + App\Infrastructure\Persistence\Doctrine\Entities\User: + algorithm: auto + + providers: + app_user_provider: + entity: + class: App\Infrastructure\Persistence\Doctrine\Entities\User + property: email + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + + api_login: + pattern: ^/api/auth/login + stateless: true + json_login: + check_path: /api/auth/login + success_handler: lexik_jwt_authentication.handler.authentication_success + failure_handler: lexik_jwt_authentication.handler.authentication_failure + + api_register: + pattern: ^/api/auth/register + stateless: true + security: false + + api: + pattern: ^/api + stateless: true + jwt: ~ + + main: + lazy: true + provider: app_user_provider + + access_control: + - { path: ^/api/auth, roles: PUBLIC_ACCESS } + - { path: ^/api/health, roles: PUBLIC_ACCESS } + - { path: ^/api, roles: IS_AUTHENTICATED_FULLY } + +when@test: + security: + password_hashers: + App\Infrastructure\Persistence\Doctrine\Entities\User: + algorithm: auto + cost: 4 + time_cost: 3 + memory_cost: 10 +"#; + pub const SYMFONY_USER_ENTITY: &str = r#"id; + } + + public function getEmail(): Email + { + return $this->email; + } + + public function getName(): UserName + { + return $this->name; + } + + public function getPassword(): HashedPassword + { + return $this->password; + } + + public function isActive(): bool + { + return $this->isActive; + } + + public function activate(): void + { + $this->isActive = true; + $this->updatedAt = new \DateTimeImmutable(); + } + + public function deactivate(): void + { + $this->isActive = false; + $this->updatedAt = new \DateTimeImmutable(); + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): \DateTimeImmutable + { + return $this->updatedAt; + } +} +"#; + pub const SYMFONY_USER_REPOSITORY: &str = r#"userRepository->findByEmail(new Email($email)); + if ($existingUser) { + throw new \DomainException('User with this email already exists'); + } + + $userId = new UserId(Uuid::v4()->toRfc4122()); + $userEmail = new Email($email); + $userName = new UserName($name); + + // Create a temporary doctrine entity for password hashing + $tempDoctrineUser = new \App\Infrastructure\Persistence\Doctrine\Entities\User( + $userId->getValue(), + $userEmail->getValue(), + $userName->getValue(), + '' + ); + + $hashedPassword = $this->passwordHasher->hashPassword($tempDoctrineUser, $plainPassword); + $password = new HashedPassword($hashedPassword); + + $user = new User($userId, $userEmail, $userName, $password); + + $this->userRepository->save($user); + + return $user; + } + + public function getUserById(string $id): ?User + { + return $this->userRepository->findById(new UserId($id)); + } + + public function getUserByEmail(string $email): ?User + { + return $this->userRepository->findByEmail(new Email($email)); + } + + public function updateUser(User $user): void + { + $this->userRepository->save($user); + } + + public function deleteUser(User $user): void + { + $this->userRepository->delete($user); + } + + public function listUsers(int $limit = 20, int $offset = 0): array + { + return $this->userRepository->findAll($limit, $offset); + } + + public function getTotalUsersCount(): int + { + return $this->userRepository->count(); + } +} +"#; + pub const SYMFONY_AUTH_SERVICE: &str = r#"userService->createUser($email, $name, $plainPassword); + + // Convert to Doctrine entity for JWT token generation + $doctrineUser = \App\Infrastructure\Persistence\Doctrine\Entities\User::fromDomain($user); + $token = $this->jwtManager->create($doctrineUser); + + return [ + 'user' => $user, + 'token' => $token + ]; + } + + public function authenticate(string $email, string $plainPassword): array + { + $user = $this->userService->getUserByEmail($email); + + if (!$user || !$user->isActive()) { + throw new \InvalidArgumentException('Invalid credentials'); + } + + // Convert to Doctrine entity for password verification + $doctrineUser = \App\Infrastructure\Persistence\Doctrine\Entities\User::fromDomain($user); + + if (!$this->passwordHasher->isPasswordValid($doctrineUser, $plainPassword)) { + throw new \InvalidArgumentException('Invalid credentials'); + } + + $token = $this->jwtManager->create($doctrineUser); + + return [ + 'user' => $user, + 'token' => $token + ]; + } +} +"#; + pub const SYMFONY_CREATE_USER_COMMAND: &str = r#"userService->createUser( + $command->email, + $command->name, + $command->password + ); + } + + public function handleGetUser(GetUserQuery $query): ?User + { + return $this->userService->getUserById($query->userId); + } +} +"#; + pub const SYMFONY_LOGIN_COMMAND: &str = r#"authService->authenticate($command->email, $command->password); + } + + public function handleRegister(CreateUserCommand $command): array + { + return $this->authService->register( + $command->email, + $command->name, + $command->password + ); + } +} +"#; + pub const SYMFONY_DOCTRINE_USER_REPOSITORY: &str = r#"findDoctrineUserById($user->getId()->getValue()); + + if ($doctrineUser) { + // Update existing + $doctrineUser = DoctrineUser::fromDomain($user); + } else { + // Create new + $doctrineUser = DoctrineUser::fromDomain($user); + $this->getEntityManager()->persist($doctrineUser); + } + + $this->getEntityManager()->flush(); + } + + public function findById(UserId $id): ?User + { + $doctrineUser = $this->findDoctrineUserById($id->getValue()); + + return $doctrineUser ? $doctrineUser->toDomain() : null; + } + + public function findByEmail(Email $email): ?User + { + $doctrineUser = $this->findOneBy(['email' => $email->getValue()]); + + return $doctrineUser ? $doctrineUser->toDomain() : null; + } + + public function delete(User $user): void + { + $doctrineUser = $this->findDoctrineUserById($user->getId()->getValue()); + + if ($doctrineUser) { + $this->getEntityManager()->remove($doctrineUser); + $this->getEntityManager()->flush(); + } + } + + public function findAll(int $limit = 20, int $offset = 0): array + { + $doctrineUsers = $this->createQueryBuilder('u') + ->setMaxResults($limit) + ->setFirstResult($offset) + ->orderBy('u.createdAt', 'DESC') + ->getQuery() + ->getResult(); + + return array_map(fn(DoctrineUser $user) => $user->toDomain(), $doctrineUsers); + } + + public function count(): int + { + return $this->createQueryBuilder('u') + ->select('COUNT(u.id)') + ->getQuery() + ->getSingleScalarResult(); + } + + private function findDoctrineUserById(string $id): ?DoctrineUser + { + return $this->find($id); + } +} +"#; + #[allow(dead_code)] + pub const SYMFONY_DOCTRINE_USER_ENTITY: &str = r#"id = $id; + $this->email = $email; + $this->name = $name; + $this->password = $password; + $this->createdAt = new \DateTimeImmutable(); + $this->updatedAt = new \DateTimeImmutable(); + } + + public static function fromDomain(DomainUser $domainUser): self + { + $user = new self( + $domainUser->getId()->getValue(), + $domainUser->getEmail()->getValue(), + $domainUser->getName()->getValue(), + $domainUser->getPassword()->getValue() + ); + + $user->isActive = $domainUser->isActive(); + $user->createdAt = $domainUser->getCreatedAt(); + $user->updatedAt = $domainUser->getUpdatedAt(); + + return $user; + } + + public function toDomain(): DomainUser + { + return new DomainUser( + new UserId($this->id), + new Email($this->email), + new UserName($this->name), + new HashedPassword($this->password), + $this->isActive, + $this->createdAt, + $this->updatedAt + ); + } + + public function getId(): string + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getName(): string + { + return $this->name; + } + + public function getUserIdentifier(): string + { + return $this->email; + } + + public function getRoles(): array + { + $roles = $this->roles; + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): void + { + $this->roles = $roles; + } + + public function getPassword(): string + { + return $this->password; + } + + public function eraseCredentials(): void + { + // Implement if needed + } + + public function isActive(): bool + { + return $this->isActive; + } + + public function setIsActive(bool $isActive): void + { + $this->isActive = $isActive; + $this->updatedAt = new \DateTimeImmutable(); + } + + public function getCreatedAt(): \DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): \DateTimeImmutable + { + return $this->updatedAt; + } +} +"#; + + pub const SYMFONY_USER_CONTROLLER: &str = r#"getContent(), true); + + $constraints = new Assert\Collection([ + 'email' => [new Assert\NotBlank(), new Assert\Email()], + 'name' => [new Assert\NotBlank(), new Assert\Length(min: 2, max: 100)], + 'password' => [new Assert\NotBlank(), new Assert\Length(min: 8)] + ]); + + $violations = $this->validator->validate($data, $constraints); + if (count($violations) > 0) { + return $this->json(['errors' => (string) $violations], Response::HTTP_BAD_REQUEST); + } + + try { + $command = new CreateUserCommand($data['email'], $data['name'], $data['password']); + $result = $this->authHandler->handleRegister($command); + + return $this->json([ + 'user' => [ + 'id' => $result['user']->getId()->getValue(), + 'email' => $result['user']->getEmail()->getValue(), + 'name' => $result['user']->getName()->getValue(), + ], + 'token' => $result['token'] + ], Response::HTTP_CREATED); + } catch (\DomainException $e) { + return $this->json(['error' => $e->getMessage()], Response::HTTP_CONFLICT); + } + } + + #[Route('/auth/login', name: 'auth_login', methods: ['POST'])] + public function login(Request $request): JsonResponse + { + $data = json_decode($request->getContent(), true); + + $constraints = new Assert\Collection([ + 'email' => [new Assert\NotBlank(), new Assert\Email()], + 'password' => [new Assert\NotBlank()] + ]); + + $violations = $this->validator->validate($data, $constraints); + if (count($violations) > 0) { + return $this->json(['errors' => (string) $violations], Response::HTTP_BAD_REQUEST); + } + + try { + $command = new LoginCommand($data['email'], $data['password']); + $result = $this->authHandler->handleLogin($command); + + return $this->json([ + 'user' => [ + 'id' => $result['user']->getId()->getValue(), + 'email' => $result['user']->getEmail()->getValue(), + 'name' => $result['user']->getName()->getValue(), + ], + 'token' => $result['token'] + ]); + } catch (\InvalidArgumentException $e) { + return $this->json(['error' => 'Invalid credentials'], Response::HTTP_UNAUTHORIZED); + } + } + + #[Route('/users/{id}', name: 'get_user', methods: ['GET'])] + public function getUser(string $id): JsonResponse + { + $query = new GetUserQuery($id); + $user = $this->userHandler->handleGetUser($query); + + if (!$user) { + return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } + + return $this->json([ + 'id' => $user->getId()->getValue(), + 'email' => $user->getEmail()->getValue(), + 'name' => $user->getName()->getValue(), + 'isActive' => $user->isActive(), + 'createdAt' => $user->getCreatedAt()->format('c'), + 'updatedAt' => $user->getUpdatedAt()->format('c'), + ]); + } + + #[Route('/health', name: 'health_check', methods: ['GET'])] + public function healthCheck(): JsonResponse + { + return $this->json([ + 'status' => 'healthy', + 'service' => '{{project_name}} API', + 'timestamp' => (new \DateTimeImmutable())->format('c') + ]); + } +} +"#; + + // Symfony Value Objects + #[allow(dead_code)] + pub const SYMFONY_USER_ID_VALUE_OBJECT: &str = r#"value)) { + throw new \InvalidArgumentException('Invalid user ID format'); + } + } + + public static function generate(): self + { + return new self(Uuid::v4()->toRfc4122()); + } + + public function getValue(): string + { + return $this->value; + } + + public function equals(UserId $other): bool + { + return $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} +"#; + + #[allow(dead_code)] + pub const SYMFONY_EMAIL_VALUE_OBJECT: &str = r#"value, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException('Invalid email format'); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function equals(Email $other): bool + { + return $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} +"#; + + #[allow(dead_code)] + pub const SYMFONY_USER_NAME_VALUE_OBJECT: &str = r#"value))) { + throw new \InvalidArgumentException('User name cannot be empty'); + } + + if (strlen($this->value) < 2 || strlen($this->value) > 100) { + throw new \InvalidArgumentException('User name must be between 2 and 100 characters'); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function equals(UserName $other): bool + { + return $this->value === $other->value; + } + + public function __toString(): string + { + return $this->value; + } +} +"#; + + #[allow(dead_code)] + pub const SYMFONY_HASHED_PASSWORD_VALUE_OBJECT: &str = r#"value)) { + throw new \InvalidArgumentException('Hashed password cannot be empty'); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function equals(HashedPassword $other): bool + { + return $this->value === $other->value; + } +} +"#; + // .env.docker for secure environment variables + pub const PHP_ENV_DOCKER: &str = r#"# Docker Environment Variables +# These should be kept secure and not committed to version control + +# Application secrets +APP_KEY=base64:$(openssl rand -base64 32) +APP_SECRET=$(openssl rand -hex 32) +JWT_SECRET=$(openssl rand -hex 32) +JWT_PASSPHRASE=$(openssl rand -hex 16) + +# Database credentials +DB_PASSWORD=$(openssl rand -hex 16) +POSTGRES_PASSWORD=${DB_PASSWORD} + +# Other secrets +REDIS_PASSWORD= +MAIL_PASSWORD= +"#; + + // Docker Compose Development Override + pub const PHP_DOCKER_COMPOSE_DEV: &str = r#"# Development overrides for docker-compose.yml +# Usage: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up + +services: + app: + build: + target: development + environment: + - APP_ENV=local + - APP_DEBUG=true + volumes: + - .:/var/www/html + - ./docker/php/local.ini:/usr/local/etc/php/conf.d/local.ini + + adminer: + image: adminer:latest + container_name: {{kebab_case}}-adminer + ports: + - "8080:8080" + environment: + - ADMINER_DEFAULT_SERVER=postgres + depends_on: + postgres: + condition: service_healthy + networks: + - {{kebab_case}}-network + restart: unless-stopped + + postgres: + ports: + - "5432:5432" + + redis: + ports: + - "6379:6379" + + mailhog: + ports: + - "1025:1025" + - "8025:8025" +"#; + + // Optimized Dockerfile for PHP + pub const PHP_OPTIMIZED_DOCKERFILE: &str = r#"# Multi-stage Dockerfile for PHP applications +FROM php:8.2-fpm-alpine AS base + +# Install system dependencies +RUN apk add --no-cache \ + nginx \ + supervisor \ + curl \ + postgresql-dev \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + libzip-dev \ + oniguruma-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) gd \ + && docker-php-ext-install pdo pdo_pgsql zip bcmath opcache + +# Install Composer +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# Set working directory +WORKDIR /var/www/html + +# Copy composer files +COPY composer.json composer.lock ./ + +# Development stage +FROM base AS development +RUN composer install --prefer-dist --no-scripts --no-autoloader +COPY . . +RUN composer dump-autoload --optimize + +# Production stage +FROM base AS production +RUN composer install --no-dev --optimize-autoloader --no-scripts +COPY . . +RUN composer dump-autoload --optimize --classmap-authoritative \ + && php artisan config:cache \ + && php artisan route:cache \ + && php artisan view:cache + +# Set permissions +RUN chown -R www-data:www-data /var/www/html \ + && chmod -R 755 /var/www/html/storage + +# Copy supervisor config +COPY docker/php/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +EXPOSE 9000 + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] +"#; + + // Nginx Dockerfile + pub const PHP_NGINX_DOCKERFILE: &str = r#"FROM nginx:alpine + +# Copy nginx configuration +COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf +COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf + +# Create log directory +RUN mkdir -p /var/log/nginx + +EXPOSE 80 443 + +CMD ["nginx", "-g", "daemon off;"] +"#; + + pub const SYMFONY_AUTH_CONTROLLER: &str = r#""#; + pub const SYMFONY_AUTH_FUNCTIONAL_TEST: &str = r#", + + /// Include MySQL configuration instead of PostgreSQL + #[arg(long)] + with_mysql: bool, + + /// Skip Docker files generation + #[arg(long)] + no_docker: bool, + }, + + /// Generate a Symfony PHP project boilerplate with Hexagonal Architecture + Symfony { + /// Project name + name: String, + + /// Output directory (defaults to project name) + directory: Option, + + /// Include MySQL configuration instead of PostgreSQL + #[arg(long)] + with_mysql: bool, + + /// Skip Docker files generation + #[arg(long)] + no_docker: bool, + }, } #[derive(clap::ValueEnum, Debug, Clone)] diff --git a/src/cli/commands.rs b/src/cli/commands.rs index 52b1c47..1bce05d 100644 --- a/src/cli/commands.rs +++ b/src/cli/commands.rs @@ -3,8 +3,9 @@ use std::path::Path; use crate::athena::{generate_docker_compose, parse_athena_file, AthenaError, AthenaResult}; use crate::boilerplate::{ - generate_fastapi_project, generate_flask_project, generate_go_project, DatabaseType, - GoFramework, ProjectConfig, + generate_fastapi_project, generate_flask_project, generate_go_project, + generate_laravel_project, generate_symfony_project, + DatabaseType, GoFramework, ProjectConfig, }; use crate::cli::args::{Commands, InitCommands}; use crate::cli::utils::{auto_detect_ath_file, should_be_verbose}; @@ -213,6 +214,72 @@ fn execute_init(init_cmd: InitCommands, verbose: bool) -> AthenaResult<()> { generate_go_project(&config)?; Ok(()) } + + InitCommands::Laravel { + name, + directory, + with_mysql, + no_docker, + } => { + let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; + if verbose { + println!("Initializing Laravel project with {}: {}", db_type, name); + } + + // Choose database type + let database = if with_mysql { + DatabaseType::MySQL + } else { + DatabaseType::PostgreSQL + }; + + // Determine directory + let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); + + let config = ProjectConfig { + name: name.clone(), + directory: project_dir.to_string_lossy().to_string(), + database, + include_docker: !no_docker, + framework: None, // Not applicable for Laravel + }; + + generate_laravel_project(&config)?; + Ok(()) + } + + InitCommands::Symfony { + name, + directory, + with_mysql, + no_docker, + } => { + let db_type = if with_mysql { "MySQL" } else { "PostgreSQL" }; + if verbose { + println!("Initializing Symfony project with {}: {}", db_type, name); + } + + // Choose database type + let database = if with_mysql { + DatabaseType::MySQL + } else { + DatabaseType::PostgreSQL + }; + + // Determine directory + let project_dir = directory.unwrap_or_else(|| Path::new(&name).to_path_buf()); + + let config = ProjectConfig { + name: name.clone(), + directory: project_dir.to_string_lossy().to_string(), + database, + include_docker: !no_docker, + framework: None, // Not applicable for Symfony + }; + + generate_symfony_project(&config)?; + Ok(()) + } } } From 8b70c0068e9bc3b282926852098f46b39754893b Mon Sep 17 00:00:00 2001 From: Jeck0v Date: Wed, 15 Oct 2025 10:13:55 +0200 Subject: [PATCH 3/3] :white_check_mark: Add Test for boilerplates PHP --- docker-compose.yml | 56 ---- .../integration/boilerplate/laravel_tests.rs | 235 +++++++++++++++ tests/integration/boilerplate/mod.rs | 28 ++ .../integration/boilerplate/symfony_tests.rs | 285 ++++++++++++++++++ 4 files changed, 548 insertions(+), 56 deletions(-) delete mode 100644 docker-compose.yml create mode 100644 tests/integration/boilerplate/laravel_tests.rs create mode 100644 tests/integration/boilerplate/symfony_tests.rs diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 3246667..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Athena v0.1.0 from test_no_conflicts deployment -# Developed by UNFAIR Team: https://github.com/Jeck0v/Athena -# Generated: 2025-10-15 08:04:01 UTC -# Features: Intelligent defaults, optimized networking, enhanced health checks - -# Services: 3 configured with intelligent defaults - -services: - app3: - image: apache:latest - container_name: test-no-conflicts-app3 - ports: - - 9000:80 - restart: always - networks: - - test_no_conflicts_network - pull_policy: missing - labels: - athena.service: app3 - athena.project: test_no_conflicts - athena.type: proxy - athena.generated: 2025-10-15 - - app2: - image: httpd:alpine - container_name: test-no-conflicts-app2 - ports: - - 8081:8000 - restart: unless-stopped - networks: - - test_no_conflicts_network - pull_policy: missing - labels: - athena.service: app2 - athena.project: test_no_conflicts - athena.type: generic - athena.generated: 2025-10-15 - - app1: - image: nginx:alpine - container_name: test-no-conflicts-app1 - ports: - - 8080:80 - restart: always - networks: - - test_no_conflicts_network - pull_policy: missing - labels: - athena.generated: 2025-10-15 - athena.project: test_no_conflicts - athena.service: app1 - athena.type: proxy -networks: - test_no_conflicts_network: - driver: bridge -name: test_no_conflicts \ No newline at end of file diff --git a/tests/integration/boilerplate/laravel_tests.rs b/tests/integration/boilerplate/laravel_tests.rs new file mode 100644 index 0000000..e56e5f2 --- /dev/null +++ b/tests/integration/boilerplate/laravel_tests.rs @@ -0,0 +1,235 @@ +use super::*; +use serial_test::serial; +use tempfile::TempDir; + +#[test] +#[serial] +fn test_laravel_init_basic() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_basic"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Laravel project")) + .stdout(predicate::str::contains(project_name)); + + // Verify project structure was created + let project_dir = temp_dir.path().join(project_name); + assert!(project_dir.exists(), "Project directory should be created"); + + // Check for Laravel files that we know exist + let expected_files = &[ + "composer.json", + "docker-compose.yml", + ".env.docker.example", + "README.md", + "docker/php/Dockerfile", + "docker/nginx/Dockerfile", + "app/Domain/User/Entities/User.php", + "app/Application/User/Commands/CreateUserCommand.php", + "app/Infrastructure/Http/Controllers/Api/V1/AuthController.php", + ]; + + for file in expected_files { + assert!(project_dir.join(file).exists(), "{} should exist", file); + } +} + +#[test] +#[serial] +fn test_laravel_docker_compose_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_docker"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check docker-compose.yml contains production-ready configuration + let docker_compose_path = project_dir.join("docker-compose.yml"); + assert!(docker_compose_path.exists(), "docker-compose.yml should exist"); + + let docker_compose_content = fs::read_to_string(&docker_compose_path) + .expect("Should be able to read docker-compose.yml"); + + // Check for production-ready features + assert!(docker_compose_content.contains("env_file:"), "Should use env_file for security"); + assert!(docker_compose_content.contains("expose:"), "Should use expose instead of ports for internal services"); + assert!(docker_compose_content.contains("healthcheck:"), "Should have health checks"); + assert!(docker_compose_content.contains("depends_on:"), "Should have service dependencies"); + assert!(docker_compose_content.contains("restart: unless-stopped"), "Should have restart policy"); +} + +#[test] +#[serial] +fn test_laravel_clean_architecture_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_architecture"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check for Clean Architecture directories + let architecture_dirs = &[ + "app/Domain", + "app/Domain/User", + "app/Domain/User/Entities", + "app/Domain/User/Repositories", + "app/Domain/User/Services", + "app/Application", + "app/Application/User/Commands", + "app/Application/User/Queries", + "app/Application/User/Handlers", + "app/Infrastructure", + "app/Infrastructure/Http/Controllers", + "app/Infrastructure/Persistence/Eloquent", + ]; + + for dir in architecture_dirs { + assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); + } +} + +#[test] +#[serial] +fn test_laravel_jwt_authentication() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_jwt"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check composer.json contains JWT dependency + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("tymon/jwt-auth"), "Should include JWT authentication package"); + + // Check for JWT-related files + assert!(project_dir.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php").exists(), + "AuthController should exist"); + + let auth_controller_path = project_dir.join("app/Infrastructure/Http/Controllers/Api/V1/AuthController.php"); + let auth_content = fs::read_to_string(&auth_controller_path) + .expect("Should be able to read AuthController"); + + assert!(auth_content.contains("login"), "AuthController should have login method"); + assert!(auth_content.contains("register"), "AuthController should have register method"); + assert!(auth_content.contains("logout"), "AuthController should have logout method"); +} + +#[test] +#[serial] +fn test_laravel_environment_security() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_security"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check .env.docker.example exists with secure variables + let env_example_path = project_dir.join(".env.docker.example"); + assert!(env_example_path.exists(), ".env.docker.example should exist"); + + let env_content = fs::read_to_string(&env_example_path) + .expect("Should be able to read .env.docker.example"); + + // Should contain variable templates, not actual secrets + assert!(env_content.contains("APP_KEY="), "Should have APP_KEY template"); + assert!(env_content.contains("DB_PASSWORD="), "Should have DB_PASSWORD template"); + assert!(env_content.contains("JWT_SECRET="), "Should have JWT_SECRET template"); + assert!(env_content.contains("openssl rand"), "Should use openssl for secret generation"); +} + +#[test] +#[serial] +fn test_laravel_testing_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_testing"; + + let mut cmd = run_init_command("laravel", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check for testing configuration + assert!(project_dir.join("phpunit.xml").exists(), "phpunit.xml should exist"); + + // Check for test directories + let test_dirs = &[ + "tests/Unit", + "tests/Feature", + "tests/Integration", + ]; + + for dir in test_dirs { + assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); + } + + // Check composer.json contains testing dependencies + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("phpunit/phpunit"), "Should include PHPUnit"); +} + +#[test] +#[serial] +fn test_laravel_no_docker() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_laravel_no_docker"; + + let mut cmd = run_init_command("laravel", project_name, &["--no-docker"]); + cmd.current_dir(&temp_dir); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Laravel project")); + + let project_dir = temp_dir.path().join(project_name); + assert!(project_dir.exists(), "Project directory should be created"); + + // Docker files should NOT exist + assert!(!project_dir.join("docker-compose.yml").exists(), + "docker-compose.yml should not exist with --no-docker"); + assert!(!project_dir.join("docker/php/Dockerfile").exists(), + "Dockerfile should not exist with --no-docker"); + assert!(!project_dir.join(".env.docker.example").exists(), + ".env.docker.example should not exist with --no-docker"); + + // But regular Laravel files should exist + assert!(project_dir.join("composer.json").exists(), "composer.json should exist"); +} + +#[test] +fn test_laravel_init_help() { + let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); + cmd.arg("init").arg("laravel").arg("--help"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Laravel")) + .stdout(predicate::str::contains("--no-docker")); +} \ No newline at end of file diff --git a/tests/integration/boilerplate/mod.rs b/tests/integration/boilerplate/mod.rs index 2f16854..a116559 100644 --- a/tests/integration/boilerplate/mod.rs +++ b/tests/integration/boilerplate/mod.rs @@ -7,6 +7,8 @@ pub mod fastapi_tests; pub mod flask_tests; pub mod go_tests; pub mod common_tests; +pub mod laravel_tests; +pub mod symfony_tests; // Common test utilities for boilerplate generation tests @@ -117,4 +119,30 @@ pub fn check_for_echo_configuration(project_dir: &Path) -> bool { check_directory_contains_any(project_dir, &["cmd", "internal", "pkg"], &[ "labstack/echo", "echo.New", "echo.Echo" ]) +} + +// PHP-specific helper functions + +pub fn check_for_laravel_configuration(project_dir: &Path) -> bool { + check_file_contains_any(project_dir, &[ + "composer.json", "config/app.php", "artisan" + ], &["laravel/framework", "Laravel", "Illuminate"]) +} + +pub fn check_for_symfony_configuration(project_dir: &Path) -> bool { + check_file_contains_any(project_dir, &[ + "composer.json", "config/services.yaml", "bin/console" + ], &["symfony/framework-bundle", "Symfony", "symfony/console"]) +} + +pub fn check_for_jwt_configuration(project_dir: &Path) -> bool { + check_file_contains_any(project_dir, &[ + "composer.json", "config/jwt.php", "config/packages/lexik_jwt_authentication.yaml" + ], &["tymon/jwt-auth", "lexik/jwt-authentication-bundle", "JWT_SECRET", "jwt"]) +} + +pub fn check_for_doctrine_configuration(project_dir: &Path) -> bool { + check_file_contains_any(project_dir, &[ + "composer.json", "config/packages/doctrine.yaml" + ], &["doctrine/orm", "doctrine/doctrine-bundle", "doctrine/migrations"]) } \ No newline at end of file diff --git a/tests/integration/boilerplate/symfony_tests.rs b/tests/integration/boilerplate/symfony_tests.rs new file mode 100644 index 0000000..0fe0d2f --- /dev/null +++ b/tests/integration/boilerplate/symfony_tests.rs @@ -0,0 +1,285 @@ +use super::*; +use serial_test::serial; +use tempfile::TempDir; + +#[test] +#[serial] +fn test_symfony_init_basic() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_basic"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Symfony project")) + .stdout(predicate::str::contains(project_name)); + + // Verify project structure was created + let project_dir = temp_dir.path().join(project_name); + assert!(project_dir.exists(), "Project directory should be created"); + + // Check for Symfony files that we know exist + let expected_files = &[ + "composer.json", + "docker-compose.yml", + ".env.docker.example", + "README.md", + "docker/php/Dockerfile", + "docker/nginx/Dockerfile", + "src/Domain/User/Entity/User.php", + "src/Application/User/Command/CreateUserCommand.php", + "src/Infrastructure/Http/Controller/Api/V1/AuthController.php", + ]; + + for file in expected_files { + assert!(project_dir.join(file).exists(), "{} should exist", file); + } +} + +#[test] +#[serial] +fn test_symfony_docker_compose_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_docker"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check docker-compose.yml contains production-ready configuration + let docker_compose_path = project_dir.join("docker-compose.yml"); + assert!(docker_compose_path.exists(), "docker-compose.yml should exist"); + + let docker_compose_content = fs::read_to_string(&docker_compose_path) + .expect("Should be able to read docker-compose.yml"); + + // Check for production-ready features + assert!(docker_compose_content.contains("env_file:"), "Should use env_file for security"); + assert!(docker_compose_content.contains("expose:"), "Should use expose instead of ports for internal services"); + assert!(docker_compose_content.contains("healthcheck:"), "Should have health checks"); + assert!(docker_compose_content.contains("depends_on:"), "Should have service dependencies"); + assert!(docker_compose_content.contains("restart: unless-stopped"), "Should have restart policy"); +} + +#[test] +#[serial] +fn test_symfony_hexagonal_architecture_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_architecture"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check for Hexagonal Architecture directories + let architecture_dirs = &[ + "src/Domain", + "src/Domain/User", + "src/Domain/User/Entity", + "src/Domain/User/Repository", + "src/Domain/User/Service", + "src/Application", + "src/Application/User/Command", + "src/Application/User/Query", + "src/Application/User/Handler", + "src/Infrastructure", + "src/Infrastructure/Http/Controller", + "src/Infrastructure/Persistence/Doctrine", + ]; + + for dir in architecture_dirs { + assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); + } +} + +#[test] +#[serial] +fn test_symfony_jwt_authentication() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_jwt"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check composer.json contains JWT dependencies + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("lexik/jwt-authentication-bundle"), + "Should include JWT authentication bundle"); + assert!(composer_content.contains("gesdinet/jwt-refresh-token-bundle"), + "Should include JWT refresh token bundle"); + + // Check for JWT-related files + assert!(project_dir.join("src/Infrastructure/Http/Controller/Api/V1/AuthController.php").exists(), + "AuthController should exist"); +} + +#[test] +#[serial] +fn test_symfony_doctrine_configuration() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_doctrine"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check composer.json contains Doctrine dependencies + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("doctrine/orm"), "Should include Doctrine ORM"); + assert!(composer_content.contains("doctrine/doctrine-bundle"), "Should include Doctrine bundle"); + assert!(composer_content.contains("doctrine/doctrine-migrations-bundle"), + "Should include Doctrine migrations"); + + // Check for Doctrine configuration files + assert!(project_dir.join("config/packages/doctrine.yaml").exists(), + "Doctrine configuration should exist"); +} + +#[test] +#[serial] +fn test_symfony_environment_security() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_security"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check .env.docker.example exists with secure variables + let env_example_path = project_dir.join(".env.docker.example"); + assert!(env_example_path.exists(), ".env.docker.example should exist"); + + let env_content = fs::read_to_string(&env_example_path) + .expect("Should be able to read .env.docker.example"); + + // Should contain variable templates, not actual secrets + assert!(env_content.contains("APP_SECRET="), "Should have APP_SECRET template"); + assert!(env_content.contains("DB_PASSWORD="), "Should have DB_PASSWORD template"); + assert!(env_content.contains("JWT_SECRET="), "Should have JWT_SECRET template"); + assert!(env_content.contains("openssl rand"), "Should use openssl for secret generation"); +} + +#[test] +#[serial] +fn test_symfony_testing_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_testing"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check for testing configuration + assert!(project_dir.join("phpunit.xml.dist").exists(), "phpunit.xml.dist should exist"); + + // Check composer.json contains testing dependencies + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("phpunit/phpunit"), "Should include PHPUnit"); + assert!(composer_content.contains("symfony/phpunit-bridge"), "Should include Symfony PHPUnit bridge"); + assert!(composer_content.contains("symfony/test-pack"), "Should include Symfony test pack"); + + // Check for test directories + let test_dirs = &[ + "tests/Unit", + "tests/Functional", + ]; + + for dir in test_dirs { + assert!(project_dir.join(dir).exists(), "{} directory should exist", dir); + } + + // Should have testing scripts in composer.json + assert!(composer_content.contains("\"test\""), "Should have test script"); +} + +#[test] +#[serial] +fn test_symfony_no_docker() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_no_docker"; + + let mut cmd = run_init_command("symfony", project_name, &["--no-docker"]); + cmd.current_dir(&temp_dir); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Symfony project")); + + let project_dir = temp_dir.path().join(project_name); + assert!(project_dir.exists(), "Project directory should be created"); + + // Docker files should NOT exist + assert!(!project_dir.join("docker-compose.yml").exists(), + "docker-compose.yml should not exist with --no-docker"); + assert!(!project_dir.join("docker/php/Dockerfile").exists(), + "Dockerfile should not exist with --no-docker"); + assert!(!project_dir.join(".env.docker.example").exists(), + ".env.docker.example should not exist with --no-docker"); + + // But regular Symfony files should exist + assert!(project_dir.join("composer.json").exists(), "composer.json should exist"); +} + +#[test] +#[serial] +fn test_symfony_api_structure() { + let temp_dir = TempDir::new().expect("Failed to create temp directory"); + let project_name = "test_symfony_api"; + + let mut cmd = run_init_command("symfony", project_name, &[]); + cmd.current_dir(&temp_dir); + + cmd.assert().success(); + + let project_dir = temp_dir.path().join(project_name); + + // Check composer.json contains API-specific dependencies + let composer_path = project_dir.join("composer.json"); + let composer_content = fs::read_to_string(&composer_path) + .expect("Should be able to read composer.json"); + + assert!(composer_content.contains("symfony/serializer"), "Should include Serializer component"); + assert!(composer_content.contains("symfony/validator"), "Should include Validator component"); + assert!(composer_content.contains("nelmio/cors-bundle"), "Should include CORS bundle"); +} + +#[test] +fn test_symfony_init_help() { + let mut cmd = Command::cargo_bin("athena").expect("Failed to find athena binary"); + cmd.arg("init").arg("symfony").arg("--help"); + + cmd.assert() + .success() + .stdout(predicate::str::contains("Symfony")) + .stdout(predicate::str::contains("--no-docker")); +} \ No newline at end of file