21 changed files with 422 additions and 676 deletions
@ -1,604 +0,0 @@ |
|||||||
# Go Implementation Plan for AutoStore |
|
||||||
|
|
||||||
## Overview |
|
||||||
Implementation of AutoStore system using Go, following Clean Architecture principles. The system stores items with expiration dates and automatically orders new items when they expire. |
|
||||||
|
|
||||||
## Architecture Approach |
|
||||||
- **Clean Architecture** with clear separation of concerns |
|
||||||
- **Domain-Driven Design** with rich domain models |
|
||||||
- **Hexagonal Architecture** with dependency inversion |
|
||||||
- **Repository Pattern** for data persistence |
|
||||||
- **CQRS-like** command/query separation |
|
||||||
- **Dependency Injection** using Go's interface system and a DI container |
|
||||||
|
|
||||||
## Core Domain Logic |
|
||||||
|
|
||||||
### ItemExpirationSpec - Single Source of Truth for Expiration |
|
||||||
|
|
||||||
**File**: `internal/domain/specifications/item_expiration_spec.go` |
|
||||||
**Purpose**: Centralized expiration checking logic - the single source of truth for determining if items are expired |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `IsExpired(item *ItemEntity, currentTime time.Time) bool` - Checks if item expired |
|
||||||
- `GetSpec(currentTime time.Time) Specification[ItemEntity]` - Returns specification for repository queries |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Called by `AddItemCommand.Execute()` to check newly created items for immediate expiration |
|
||||||
- Called by `HandleExpiredItemsCommand.Execute()` to find expired items for processing |
|
||||||
- Used by `ItemRepository.FindWhere()` to query database for expired items |
|
||||||
|
|
||||||
## Detailed Implementation Plan |
|
||||||
|
|
||||||
### Domain Layer |
|
||||||
|
|
||||||
#### 1. Entities |
|
||||||
|
|
||||||
**File**: `internal/domain/entities/item.go` |
|
||||||
**Purpose**: Core business entity representing an item |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewItem(id ItemID, name string, expirationDate ExpirationDate, orderURL string, userID UserID) (*ItemEntity, error)` - Creates item with validation |
|
||||||
- `GetID() ItemID` - Returns item ID |
|
||||||
- `GetName() string` - Returns item name |
|
||||||
- `GetExpirationDate() ExpirationDate` - Returns expiration date |
|
||||||
- `GetOrderURL() string` - Returns order URL |
|
||||||
- `GetUserID() UserID` - Returns user ID |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Created by `AddItemCommand.Execute()` |
|
||||||
- Retrieved by `ItemRepository` methods |
|
||||||
- Passed to `ItemExpirationSpec.IsExpired()` for expiration checking |
|
||||||
|
|
||||||
**File**: `internal/domain/entities/user.go` |
|
||||||
**Purpose**: User entity for item ownership and authentication purposes |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewUser(id UserID, username string, passwordHash string) (*UserEntity, error)` - Creates user with validation |
|
||||||
- `GetID() UserID` - Returns user ID |
|
||||||
- `GetUsername() string` - Returns username |
|
||||||
- `GetPasswordHash() string` - Returns password hash |
|
||||||
- `ValidatePassword(password string) bool` - Validates password |
|
||||||
|
|
||||||
#### 2. Value Objects |
|
||||||
|
|
||||||
**File**: `internal/domain/value_objects/item_id.go` |
|
||||||
**Purpose**: Strong typing for item identifiers |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewItemID(value string) (ItemID, error)` - Validates UUID format |
|
||||||
- `String() string` - Returns string value |
|
||||||
- `Equals(other ItemID) bool` - Compares with another ItemID |
|
||||||
|
|
||||||
**File**: `internal/domain/value_objects/user_id.go` |
|
||||||
**Purpose**: Strong typing for user identifiers |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewUserID(value string) (UserID, error)` - Validates UUID format |
|
||||||
- `String() string` - Returns string value |
|
||||||
- `Equals(other UserID) bool` - Compares with another UserID |
|
||||||
|
|
||||||
**File**: `internal/domain/value_objects/expiration_date.go` |
|
||||||
**Purpose**: Immutable expiration date with validation |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewExpirationDate(value time.Time) (ExpirationDate, error)` - Validates date format (allows past dates per business rules) |
|
||||||
- `Time() time.Time` - Returns time.Time object |
|
||||||
- `String() string` - Returns ISO string format |
|
||||||
- `IsExpired(currentTime time.Time) bool` - Checks if date is expired |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Used by `ItemEntity` constructor for type-safe date handling |
|
||||||
- Validated by `ItemExpirationSpec.IsExpired()` for expiration logic |
|
||||||
|
|
||||||
#### 3. Specifications |
|
||||||
|
|
||||||
**File**: `internal/domain/specifications/specification.go` |
|
||||||
**Purpose**: Generic specification pattern interface |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `IsSatisfiedBy(candidate T) bool` - Evaluates specification |
|
||||||
- `And(other Specification[T]) Specification[T]` - Combines with AND |
|
||||||
- `Or(other Specification[T]) Specification[T]` - Combines with OR |
|
||||||
- `Not() Specification[T]` - Negates specification |
|
||||||
|
|
||||||
**File**: `internal/domain/specifications/item_expiration_spec.go` |
|
||||||
**Purpose**: Implementation of expiration specification |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `IsExpired(item *ItemEntity, currentTime time.Time) bool` - Checks if item is expired |
|
||||||
- `GetSpec(currentTime time.Time) Specification[ItemEntity]` - Returns specification for repository queries |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Implemented by `ItemExpirationSpec` for type-safe specifications |
|
||||||
- Used by `ItemRepository.FindWhere()` for database queries |
|
||||||
|
|
||||||
### Application Layer |
|
||||||
|
|
||||||
#### 4. Commands |
|
||||||
|
|
||||||
**File**: `internal/application/commands/add_item_command.go` |
|
||||||
**Purpose**: Use case for creating new items with expiration handling |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewAddItemCommand(itemRepo IItemRepository, orderService IOrderService, timeProvider ITimeProvider, expirationSpec *ItemExpirationSpec, logger ILogger) *AddItemCommand` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context, name string, expirationDate time.Time, orderURL string, userID string) (string, error)` - Creates item, handles expired items immediately |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. `ItemsController.CreateItem()` calls `AddItemCommand.Execute()` |
|
||||||
2. Creates `ItemEntity` with validated data |
|
||||||
3. Calls `ItemExpirationSpec.IsExpired()` to check if item is expired |
|
||||||
4. If expired: |
|
||||||
- calls `OrderHTTPService.OrderItem()` |
|
||||||
- **returns item ID** (business rule: expired items trigger ordering but still return ID field that might be empty or invalid) |
|
||||||
5. If not expired: calls `ItemRepository.Save()` and returns item ID |
|
||||||
|
|
||||||
**File**: `internal/application/commands/handle_expired_items_command.go` |
|
||||||
**Purpose**: Background command to process expired items |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewHandleExpiredItemsCommand(itemRepo IItemRepository, orderService IOrderService, timeProvider ITimeProvider, expirationSpec *ItemExpirationSpec, logger ILogger) *HandleExpiredItemsCommand` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context) error` - Finds and processes all expired items |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. `ExpiredItemsScheduler.Run()` calls `HandleExpiredItemsCommand.Execute()` |
|
||||||
2. Gets current time from `ITimeProvider` |
|
||||||
3. Calls `ItemExpirationSpec.GetSpec()` to get expiration specification |
|
||||||
4. Calls `ItemRepository.FindWhere()` to find expired items |
|
||||||
5. For each expired item: calls `OrderHTTPService.OrderItem()` then `ItemRepository.Delete()` |
|
||||||
|
|
||||||
**File**: `internal/application/commands/delete_item_command.go` |
|
||||||
**Purpose**: Use case for deleting user items |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewDeleteItemCommand(itemRepo IItemRepository, logger ILogger) *DeleteItemCommand` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context, itemID string, userID string) error` - Validates ownership and deletes item |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. `ItemsController.DeleteItem()` calls `DeleteItemCommand.Execute()` |
|
||||||
2. Calls `ItemRepository.FindByID()` to retrieve item |
|
||||||
3. Validates ownership by comparing user IDs |
|
||||||
4. Calls `ItemRepository.Delete()` to remove item |
|
||||||
|
|
||||||
**File**: `internal/application/commands/login_user_command.go` |
|
||||||
**Purpose**: User authentication use case |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewLoginUserCommand(authService IAuthService, logger ILogger) *LoginUserCommand` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context, username string, password string) (string, error)` - Authenticates and returns JWT token |
|
||||||
|
|
||||||
#### 5. Queries |
|
||||||
|
|
||||||
**File**: `internal/application/queries/get_item_query.go` |
|
||||||
**Purpose**: Retrieves single item by ID with authorization |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewGetItemQuery(itemRepo IItemRepository, logger ILogger) *GetItemQuery` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context, itemID string, userID string) (*ItemEntity, error)` - Validates ownership and returns item |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. `ItemsController.GetItem()` calls `GetItemQuery.Execute()` |
|
||||||
2. Calls `ItemRepository.FindByID()` to retrieve item |
|
||||||
3. Validates ownership by comparing user IDs |
|
||||||
4. Returns item entity |
|
||||||
|
|
||||||
**File**: `internal/application/queries/list_items_query.go` |
|
||||||
**Purpose**: Retrieves all items for authenticated user |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewListItemsQuery(itemRepo IItemRepository, logger ILogger) *ListItemsQuery` - Constructor with dependency injection |
|
||||||
- `Execute(ctx context.Context, userID string) ([]*ItemEntity, error)` - Returns user's items |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. `ItemsController.ListItems()` calls `ListItemsQuery.Execute()` |
|
||||||
2. Calls `ItemRepository.FindByUserID()` to retrieve user's items |
|
||||||
3. Returns array of item entities |
|
||||||
|
|
||||||
#### 6. DTOs |
|
||||||
|
|
||||||
**File**: `internal/application/dto/create_item_dto.go` |
|
||||||
**Purpose**: Request validation for item creation |
|
||||||
|
|
||||||
**Key Properties**: |
|
||||||
- `Name string` - Item name (validation: not empty, max 255 chars) |
|
||||||
- `ExpirationDate time.Time` - Date (validation: valid date) |
|
||||||
- `OrderURL string` - Order URL (validation: valid URL format) |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Validate() error` - Validates DTO fields |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Used by `ItemsController.CreateItem()` for request body validation |
|
||||||
|
|
||||||
**File**: `internal/application/dto/item_response_dto.go` |
|
||||||
**Purpose**: Standardized item response format |
|
||||||
|
|
||||||
**Key Properties**: |
|
||||||
- `ID string` - Item ID |
|
||||||
- `Name string` - Item name |
|
||||||
- `ExpirationDate time.Time` - Expiration date |
|
||||||
- `OrderURL string` - Order URL |
|
||||||
- `UserID string` - Owner user ID |
|
||||||
- `CreatedAt time.Time` - Creation timestamp |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `FromEntity(item *ItemEntity) *ItemResponseDTO` - Creates DTO from entity |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Used by all item controller methods for response transformation |
|
||||||
|
|
||||||
**File**: `internal/application/dto/login_dto.go` |
|
||||||
**Purpose**: Login request validation |
|
||||||
|
|
||||||
**Key Properties**: |
|
||||||
- `Username string` - Username (validation: not empty) |
|
||||||
- `Password string` - Password (validation: not empty) |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Validate() error` - Validates DTO fields |
|
||||||
|
|
||||||
#### 7. Interfaces |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/item_repository.go` |
|
||||||
**Purpose**: Repository interface for item persistence |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Save(ctx context.Context, item *ItemEntity) error` |
|
||||||
- `FindByID(ctx context.Context, id ItemID) (*ItemEntity, error)` |
|
||||||
- `FindByUserID(ctx context.Context, userID UserID) ([]*ItemEntity, error)` |
|
||||||
- `FindWhere(ctx context.Context, spec Specification[ItemEntity]) ([]*ItemEntity, error)` |
|
||||||
- `Delete(ctx context.Context, id ItemID) error` |
|
||||||
- `Exists(ctx context.Context, id ItemID) (bool, error)` |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/user_repository.go` |
|
||||||
**Purpose**: Repository interface for user persistence |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `FindByUsername(ctx context.Context, username string) (*UserEntity, error)` |
|
||||||
- `FindByID(ctx context.Context, id UserID) (*UserEntity, error)` |
|
||||||
- `Save(ctx context.Context, user *UserEntity) error` |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/auth_service.go` |
|
||||||
**Purpose**: Authentication service interface |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Authenticate(ctx context.Context, username string, password string) (string, error)` |
|
||||||
- `ValidateToken(ctx context.Context, token string) (bool, error)` |
|
||||||
- `GetUserIDFromToken(ctx context.Context, token string) (string, error)` |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/order_service.go` |
|
||||||
**Purpose**: Order service interface |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `OrderItem(ctx context.Context, item *ItemEntity) error` |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/time_provider.go` |
|
||||||
**Purpose**: Time provider interface for testing |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Now() time.Time` |
|
||||||
|
|
||||||
**File**: `internal/application/interfaces/logger.go` |
|
||||||
**Purpose**: Logger interface |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Info(ctx context.Context, msg string, fields ...interface{})` |
|
||||||
- `Error(ctx context.Context, msg string, fields ...interface{})` |
|
||||||
- `Debug(ctx context.Context, msg string, fields ...interface{})` |
|
||||||
- `Warn(ctx context.Context, msg string, fields ...interface{})` |
|
||||||
|
|
||||||
### Infrastructure Layer |
|
||||||
|
|
||||||
#### 8. Repositories |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/repositories/file_item_repository.go` |
|
||||||
**Purpose**: File-based implementation of item repository using JSON files |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Save(ctx context.Context, item *ItemEntity) error` - Persists item entity |
|
||||||
- `FindByID(ctx context.Context, id ItemID) (*ItemEntity, error)` - Finds by ID |
|
||||||
- `FindByUserID(ctx context.Context, userID UserID) ([]*ItemEntity, error)` - Finds by user |
|
||||||
- `FindWhere(ctx context.Context, spec Specification[ItemEntity]) ([]*ItemEntity, error)` - Finds by specification using `ItemExpirationSpec` |
|
||||||
- `Delete(ctx context.Context, id ItemID) error` - Deletes item |
|
||||||
- `Exists(ctx context.Context, id ItemID) (bool, error)` - Checks existence |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Called by all commands and queries for data persistence and retrieval |
|
||||||
- Uses `ItemExpirationSpec` for finding expired items |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/repositories/file_user_repository.go` |
|
||||||
**Purpose**: File-based implementation of user repository using JSON files |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `FindByUsername(ctx context.Context, username string) (*UserEntity, error)` - Finds by username |
|
||||||
- `FindByID(ctx context.Context, id UserID) (*UserEntity, error)` - Finds by ID |
|
||||||
- `Save(ctx context.Context, user *UserEntity) error` - Saves user |
|
||||||
|
|
||||||
#### 9. HTTP Services |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/http/order_http_service.go` |
|
||||||
**Purpose**: HTTP implementation of order service |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewOrderHTTPService(client *http.Client, logger ILogger) *OrderHTTPService` - Constructor with dependency injection |
|
||||||
- `OrderItem(ctx context.Context, item *ItemEntity) error` - Sends POST request to order URL |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Called by `AddItemCommand.Execute()` for expired items |
|
||||||
- Called by `HandleExpiredItemsCommand.Execute()` for batch processing |
|
||||||
|
|
||||||
#### 10. Authentication |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/auth/jwt_auth_service.go` |
|
||||||
**Purpose**: JWT implementation of authentication service |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewJWTAuthService(userRepo IUserRepository, secretKey string, logger ILogger) *JWTAuthService` - Constructor with dependency injection |
|
||||||
- `Authenticate(ctx context.Context, username string, password string) (string, error)` - Validates credentials and generates JWT |
|
||||||
- `ValidateToken(ctx context.Context, token string) (bool, error)` - Validates JWT token |
|
||||||
- `GetUserIDFromToken(ctx context.Context, token string) (string, error)` - Extracts user ID from token |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Called by `LoginUserCommand.Execute()` for user authentication |
|
||||||
- Used by `JWTMiddleware` for route protection |
|
||||||
|
|
||||||
#### 11. Time Provider |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/time/system_time_provider.go` |
|
||||||
**Purpose**: System time implementation |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewSystemTimeProvider() *SystemTimeProvider` - Constructor |
|
||||||
- `Now() time.Time` - Returns current system time |
|
||||||
|
|
||||||
#### 12. Logger |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/logging/standard_logger.go` |
|
||||||
**Purpose**: Standard library logger implementation |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewStandardLogger(writer io.Writer) *StandardLogger` - Constructor |
|
||||||
- `Info(ctx context.Context, msg string, fields ...interface{})` - Logs info message |
|
||||||
- `Error(ctx context.Context, msg string, fields ...interface{})` - Logs error message |
|
||||||
- `Debug(ctx context.Context, msg string, fields ...interface{})` - Logs debug message |
|
||||||
- `Warn(ctx context.Context, msg string, fields ...interface{})` - Logs warning message |
|
||||||
|
|
||||||
### Presentation Layer |
|
||||||
|
|
||||||
#### 13. Controllers |
|
||||||
|
|
||||||
**File**: `internal/presentation/controllers/items_controller.go` |
|
||||||
**Purpose**: REST API endpoints for item management |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewItemsController(addItemCmd *AddItemCommand, getItemQry *GetItemQuery, listItemsQry *ListItemsQuery, deleteItemCmd *DeleteItemCommand, logger ILogger) *ItemsController` - Constructor with dependency injection |
|
||||||
- `CreateItem(c *gin.Context)` - POST /items |
|
||||||
- `GetItem(c *gin.Context)` - GET /items/:id |
|
||||||
- `ListItems(c *gin.Context)` - GET /items |
|
||||||
- `DeleteItem(c *gin.Context)` - DELETE /items/:id |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
- Receives HTTP requests and validates input |
|
||||||
- Calls appropriate commands/queries based on HTTP method |
|
||||||
- Returns standardized responses with DTOs |
|
||||||
|
|
||||||
**File**: `internal/presentation/controllers/auth_controller.go` |
|
||||||
**Purpose**: Authentication endpoints |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewAuthController(loginUserCmd *LoginUserCommand, logger ILogger) *AuthController` - Constructor with dependency injection |
|
||||||
- `Login(c *gin.Context)` - POST /login |
|
||||||
|
|
||||||
#### 14. Middleware |
|
||||||
|
|
||||||
**File**: `internal/presentation/middleware/jwt_middleware.go` |
|
||||||
**Purpose**: JWT authentication middleware |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewJWTMiddleware(authService IAuthService, logger ILogger) *JWTMiddleware` - Constructor with dependency injection |
|
||||||
- `Middleware() gin.HandlerFunc` - Returns gin middleware function |
|
||||||
|
|
||||||
**Place in the flow**: |
|
||||||
- Applied to all protected routes by Gin router group |
|
||||||
- Uses `JWTAuthService` for token validation |
|
||||||
|
|
||||||
#### 15. Server |
|
||||||
|
|
||||||
**File**: `internal/presentation/server/server.go` |
|
||||||
**Purpose**: HTTP server setup and configuration |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewServer(config *Config, logger ILogger) *Server` - Constructor with dependency injection |
|
||||||
- `Start() error` - Starts the HTTP server |
|
||||||
- `SetupRoutes() *gin.Engine` - Sets up routes and middleware |
|
||||||
|
|
||||||
### Background Processing |
|
||||||
|
|
||||||
**File**: `internal/infrastructure/scheduler/expired_items_scheduler.go` |
|
||||||
**Purpose**: Scheduled job for processing expired items using a ticker |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewExpiredItemsScheduler(handleExpiredItemsCmd *HandleExpiredItemsCommand, logger ILogger) *ExpiredItemsScheduler` - Constructor with dependency injection |
|
||||||
- `Start(ctx context.Context) error` - Starts the scheduler |
|
||||||
- `Stop() error` - Stops the scheduler |
|
||||||
- `processExpiredItems(ctx context.Context) error` - Processes expired items |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. **On startup**: `processExpiredItems()` immediately calls `HandleExpiredItemsCommand.Execute()` |
|
||||||
2. **Every minute**: Ticker triggers `processExpiredItems()` to process expired items |
|
||||||
3. All methods use context for cancellation and error handling |
|
||||||
4. Comprehensive logging for monitoring and debugging |
|
||||||
|
|
||||||
**Configuration**: |
|
||||||
- Uses `time.Ticker` for periodic execution |
|
||||||
- Implements graceful shutdown with context cancellation |
|
||||||
- Configurable interval (default: 1 minute) |
|
||||||
|
|
||||||
### Dependency Injection |
|
||||||
|
|
||||||
**File**: `internal/container/container.go` |
|
||||||
**Purpose**: Dependency injection container setup |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `NewContainer(config *Config) *Container` - Constructor |
|
||||||
- `Initialize() error` - Initializes all dependencies |
|
||||||
- `GetItemRepository() IItemRepository` - Returns item repository |
|
||||||
- `GetUserRepository() IUserRepository` - Returns user repository |
|
||||||
- `GetAuthService() IAuthService` - Returns auth service |
|
||||||
- `GetOrderService() IOrderService` - Returns order service |
|
||||||
- `GetTimeProvider() ITimeProvider` - Returns time provider |
|
||||||
- `GetLogger() ILogger` - Returns logger |
|
||||||
- `GetAddItemCommand() *AddItemCommand` - Returns add item command |
|
||||||
- `GetHandleExpiredItemsCommand() *HandleExpiredItemsCommand` - Returns handle expired items command |
|
||||||
- `GetDeleteItemCommand() *DeleteItemCommand` - Returns delete item command |
|
||||||
- `GetLoginUserCommand() *LoginUserCommand` - Returns login user command |
|
||||||
- `GetGetItemQuery() *GetItemQuery` - Returns get item query |
|
||||||
- `GetListItemsQuery() *ListItemsQuery` - Returns list items query |
|
||||||
- `GetItemsController() *ItemsController` - Returns items controller |
|
||||||
- `GetAuthController() *AuthController` - Returns auth controller |
|
||||||
- `GetJWTMiddleware() *JWTMiddleware` - Returns JWT middleware |
|
||||||
- `GetExpiredItemsScheduler() *ExpiredItemsScheduler` - Returns expired items scheduler |
|
||||||
|
|
||||||
### Configuration |
|
||||||
|
|
||||||
**File**: `internal/config/config.go` |
|
||||||
**Purpose**: Application configuration |
|
||||||
|
|
||||||
**Key Properties**: |
|
||||||
- `ServerPort int` - Server port |
|
||||||
- `JWTSecret string` - JWT secret key |
|
||||||
- `DataDirectory string` - Directory for data files |
|
||||||
- `LogLevel string` - Log level |
|
||||||
- `SchedulerInterval time.Duration` - Scheduler interval |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `Load() (*Config, error)` - Loads configuration from environment variables |
|
||||||
- `Validate() error` - Validates configuration |
|
||||||
|
|
||||||
### Main Application |
|
||||||
|
|
||||||
**File**: `cmd/main.go` |
|
||||||
**Purpose**: Application entry point |
|
||||||
|
|
||||||
**Key Methods**: |
|
||||||
- `main()` - Main function |
|
||||||
- `setupGracefulShutdown(server *Server, scheduler *ExpiredItemsScheduler)` - Sets up graceful shutdown |
|
||||||
|
|
||||||
**Flow**: |
|
||||||
1. Loads configuration |
|
||||||
2. Initializes dependency container |
|
||||||
3. Creates server and scheduler |
|
||||||
4. Starts scheduler |
|
||||||
5. Starts server |
|
||||||
6. Sets up graceful shutdown |
|
||||||
|
|
||||||
## Complete Flow Summary |
|
||||||
|
|
||||||
### Item Creation Flow |
|
||||||
``` |
|
||||||
POST /items |
|
||||||
├── JWTMiddleware (authentication) |
|
||||||
├── CreateItemDTO validation |
|
||||||
├── ItemsController.CreateItem() |
|
||||||
│ ├── AddItemCommand.Execute() |
|
||||||
│ │ ├── ItemEntity constructor (validation) |
|
||||||
│ │ ├── ItemExpirationSpec.IsExpired() ← SINGLE SOURCE OF TRUTH |
|
||||||
│ │ ├── If expired: OrderHTTPService.OrderItem() |
|
||||||
│ │ └── If not expired: ItemRepository.Save() |
|
||||||
│ └── ItemResponseDTO transformation |
|
||||||
└── HTTP response |
|
||||||
``` |
|
||||||
|
|
||||||
### Expired Items Processing Flow |
|
||||||
``` |
|
||||||
Scheduler (every minute) |
|
||||||
└── ExpiredItemsScheduler.processExpiredItems() |
|
||||||
└── HandleExpiredItemsCommand.Execute() |
|
||||||
├── ITimeProvider.Now() |
|
||||||
├── ItemExpirationSpec.GetSpec() ← SINGLE SOURCE OF TRUTH |
|
||||||
├── ItemRepository.FindWhere() (using spec) |
|
||||||
├── For each expired item: |
|
||||||
│ ├── OrderHTTPService.OrderItem() |
|
||||||
│ └── ItemRepository.Delete() |
|
||||||
└── Logging |
|
||||||
``` |
|
||||||
|
|
||||||
### Item Retrieval Flow |
|
||||||
``` |
|
||||||
GET /items/:id |
|
||||||
├── JWTMiddleware (authentication) |
|
||||||
├── ItemsController.GetItem() |
|
||||||
│ ├── GetItemQuery.Execute() |
|
||||||
│ │ ├── ItemRepository.FindByID() |
|
||||||
│ │ ├── Ownership validation |
|
||||||
│ │ └── Return ItemEntity |
|
||||||
│ └── ItemResponseDTO transformation |
|
||||||
└── HTTP response |
|
||||||
``` |
|
||||||
|
|
||||||
## Key Design Principles |
|
||||||
|
|
||||||
1. **Single Source of Truth**: `ItemExpirationSpec` is the only component that determines expiration logic |
|
||||||
2. **Clear Flow**: Each component has a well-defined place in the execution chain |
|
||||||
3. **Dependency Inversion**: High-level modules don't depend on low-level modules |
|
||||||
4. **Separation of Concerns**: Each layer has distinct responsibilities |
|
||||||
5. **Testability**: All components can be tested in isolation |
|
||||||
6. **Context Propagation**: All methods accept context for cancellation, deadlines, and tracing |
|
||||||
7. **Error Handling**: Comprehensive error handling with proper error types |
|
||||||
8. **Configuration Management**: Environment-based configuration with validation |
|
||||||
|
|
||||||
## Go-Specific Best Practices |
|
||||||
|
|
||||||
1. **Package Organization**: Follow Go's standard package layout with `internal/` for private packages |
|
||||||
2. **Error Handling**: Use Go's error handling pattern with proper error wrapping |
|
||||||
3. **Context Usage**: Use context for cancellation, deadlines, and request-scoped values |
|
||||||
4. **Interface Segregation**: Keep interfaces small and focused |
|
||||||
5. **Naming Conventions**: Follow Go's naming conventions (camelCase for exported, camelCase for unexported) |
|
||||||
6. **Pointer vs Value**: Use pointers for mutable state and large structs, values for immutable data |
|
||||||
7. **Concurrency**: Use goroutines and channels for concurrent operations |
|
||||||
8. **Testing**: Write comprehensive tests with table-driven tests |
|
||||||
9. **Documentation**: Use Go's documentation comments for all exported types and functions |
|
||||||
10. **Dependencies**: Use Go modules for dependency management |
|
||||||
|
|
||||||
## Directory Structure |
|
||||||
|
|
||||||
``` |
|
||||||
golang/ |
|
||||||
├── cmd/ |
|
||||||
│ └── main.go # Application entry point |
|
||||||
├── internal/ |
|
||||||
│ ├── application/ |
|
||||||
│ │ ├── commands/ # Command handlers |
|
||||||
│ │ ├── queries/ # Query handlers |
|
||||||
│ │ ├── dto/ # Data transfer objects |
|
||||||
│ │ └── interfaces/ # Application interfaces |
|
||||||
│ ├── domain/ |
|
||||||
│ │ ├── entities/ # Domain entities |
|
||||||
│ │ ├── value_objects/ # Value objects |
|
||||||
│ │ └── specifications/ # Domain specifications |
|
||||||
│ ├── infrastructure/ |
|
||||||
│ │ ├── repositories/ # Repository implementations |
|
||||||
│ │ ├── http/ # HTTP services |
|
||||||
│ │ ├── auth/ # Authentication services |
|
||||||
│ │ ├── time/ # Time provider |
|
||||||
│ │ ├── logging/ # Logger implementations |
|
||||||
│ │ └── scheduler/ # Background scheduler |
|
||||||
│ ├── presentation/ |
|
||||||
│ │ ├── controllers/ # HTTP controllers |
|
||||||
│ │ ├── middleware/ # HTTP middleware |
|
||||||
│ │ └── server/ # HTTP server |
|
||||||
│ ├── config/ |
|
||||||
│ │ └── config.go # Configuration |
|
||||||
│ └── container/ |
|
||||||
│ └── container.go # Dependency injection container |
|
||||||
├── pkg/ |
|
||||||
│ └── specifications/ # Reusable specification patterns |
|
||||||
├── data/ # Data files (JSON) |
|
||||||
├── docker/ |
|
||||||
│ ├── docker-compose.yml # Docker compose |
|
||||||
│ └── Dockerfile # Docker image |
|
||||||
├── go.mod # Go module file |
|
||||||
├── go.sum # Go module checksums |
|
||||||
└── README.md # Go implementation README |
|
||||||
``` |
|
||||||
|
|
||||||
This implementation plan ensures consistent development regardless of the implementer, providing clear flow definitions and emphasizing `ItemExpirationSpec` as the centralized source for expiration logic, while following Go best practices and idioms. |
|
||||||
@ -0,0 +1,39 @@ |
|||||||
|
module autostore |
||||||
|
|
||||||
|
go 1.21 |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/gin-gonic/gin v1.9.1 |
||||||
|
github.com/golang-jwt/jwt/v4 v4.5.2 |
||||||
|
github.com/google/uuid v1.3.0 |
||||||
|
github.com/stretchr/testify v1.8.3 |
||||||
|
golang.org/x/crypto v0.9.0 |
||||||
|
) |
||||||
|
|
||||||
|
require ( |
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect |
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect |
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect |
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect |
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect |
||||||
|
github.com/go-playground/locales v0.14.1 // indirect |
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect |
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect |
||||||
|
github.com/goccy/go-json v0.10.2 // indirect |
||||||
|
github.com/json-iterator/go v1.1.12 // indirect |
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect |
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect |
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect |
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect |
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect |
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect |
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect |
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect |
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect |
||||||
|
golang.org/x/arch v0.3.0 // indirect |
||||||
|
golang.org/x/net v0.10.0 // indirect |
||||||
|
golang.org/x/sys v0.8.0 // indirect |
||||||
|
golang.org/x/text v0.9.0 // indirect |
||||||
|
google.golang.org/protobuf v1.30.0 // indirect |
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect |
||||||
|
) |
||||||
@ -0,0 +1,189 @@ |
|||||||
|
# Specification Pattern in PHP for AutoStore |
||||||
|
|
||||||
|
The Specification pattern is a way to encapsulate business rules or query criteria in a single place. In AutoStore, this pattern helps us define conditions like "find expired items" without duplicating logic across the codebase. |
||||||
|
|
||||||
|
## Why Do We Need This? |
||||||
|
|
||||||
|
Imagine checking if an item is expired in multiple places: |
||||||
|
|
||||||
|
```php |
||||||
|
// In a repository |
||||||
|
$expiredItems = array_filter($items, function($item) { |
||||||
|
return $item->getExpirationDate() <= new \DateTime(); |
||||||
|
}); |
||||||
|
|
||||||
|
// In a service |
||||||
|
if ($item->getExpirationDate() <= new \DateTime()) { |
||||||
|
$this->sendOrderNotification($item); |
||||||
|
} |
||||||
|
|
||||||
|
// In SQL query |
||||||
|
$stmt = $pdo->prepare("SELECT * FROM items WHERE expirationDate <= :expirationDate"); |
||||||
|
$stmt->execute(['expirationDate' => (new DateTime())->format('Y-m-d H:i:s')]); |
||||||
|
$items = $stmt->fetchAll(PDO::FETCH_ASSOC); |
||||||
|
``` |
||||||
|
|
||||||
|
This creates problems: |
||||||
|
- **Duplication**: The same rule appears multiple times |
||||||
|
- **Maintenance**: If the rule changes, you need to update it everywhere |
||||||
|
- **Database queries**: In-memory filtering doesn't translate to SQL WHERE clauses |
||||||
|
|
||||||
|
## How This Implementation Works |
||||||
|
|
||||||
|
The implementation has two main classes: |
||||||
|
|
||||||
|
1. **Specification**: Evaluates conditions against objects |
||||||
|
2. **Spec**: Helper class with constants and methods to build specifications |
||||||
|
|
||||||
|
### Basic Structure |
||||||
|
|
||||||
|
A specification can be: |
||||||
|
- A simple comparison: `[field, operator, value]` (e.g., `['expirationDate', '<=', $today]`) |
||||||
|
- A logical group: `AND`, `OR`, or `NOT` containing other specifications |
||||||
|
|
||||||
|
### Simple Example: Finding Expired Items |
||||||
|
|
||||||
|
```php |
||||||
|
// Create a specification for expired items |
||||||
|
$expiredSpec = new Specification( |
||||||
|
Spec::lte('expirationDate', new \DateTimeImmutable()) |
||||||
|
); |
||||||
|
|
||||||
|
// Use it to check if an item is expired |
||||||
|
if ($expiredSpec->match($item)) { |
||||||
|
echo "This item is expired!"; |
||||||
|
} |
||||||
|
|
||||||
|
// Or filter an array of items |
||||||
|
$expiredItems = array_filter($items, function($item) use ($expiredSpec) { |
||||||
|
return $expiredSpec->match($item); |
||||||
|
}); |
||||||
|
``` |
||||||
|
|
||||||
|
### Creating Complex Conditions |
||||||
|
|
||||||
|
You can combine conditions using `and`, `or`, and `not`: |
||||||
|
|
||||||
|
```php |
||||||
|
// Find active items that are expiring in the next 7 days |
||||||
|
$aboutToExpireSpec = new Specification( |
||||||
|
Spec::and([ |
||||||
|
Spec::eq('status', 'active'), |
||||||
|
Spec::and([ |
||||||
|
Spec::gte('expirationDate', new \DateTimeImmutable()), |
||||||
|
Spec::lte('expirationDate', new \DateTimeImmutable('+7 days')) |
||||||
|
]) |
||||||
|
]) |
||||||
|
); |
||||||
|
``` |
||||||
|
|
||||||
|
## Available Operators |
||||||
|
|
||||||
|
### Comparison Operators |
||||||
|
|
||||||
|
| Method | Description | Example | |
||||||
|
|--------|-------------|---------| |
||||||
|
| `Spec::eq(field, value)` | Equals | `Spec::eq('name', 'Milk')` | |
||||||
|
| `Spec::neq(field, value)` | Not equals | `Spec::neq('status', 'deleted')` | |
||||||
|
| `Spec::gt(field, value)` | Greater than | `Spec::gt('quantity', 10)` | |
||||||
|
| `Spec::gte(field, value)` | Greater than or equal | `Spec::gte('price', 5.99)` | |
||||||
|
| `Spec::lt(field, value)` | Less than | `Spec::lt('weight', 2.5)` | |
||||||
|
| `Spec::lte(field, value)` | Less than or equal | `Spec::lte('expirationDate', $today)` | |
||||||
|
| `Spec::in(field, [values])` | Value is in list | `Spec::in('category', ['dairy', 'meat'])` | |
||||||
|
| `Spec::nin(field, [values])` | Value is not in list | `Spec::nin('status', ['deleted', 'archived'])` | |
||||||
|
|
||||||
|
### Logical Operators |
||||||
|
|
||||||
|
| Method | Description | Example | |
||||||
|
|--------|-------------|---------| |
||||||
|
| `Spec::and([specs])` | All conditions must be true | `Spec::and([$spec1, $spec2])` | |
||||||
|
| `Spec::or([specs])` | At least one condition must be true | `Spec::or([$spec1, $spec2])` | |
||||||
|
| `Spec::not(spec)` | Negates the condition | `Spec::not($spec1)` | |
||||||
|
|
||||||
|
## Best Practice: Domain-Specific Specifications |
||||||
|
|
||||||
|
For important business rules, create dedicated classes: |
||||||
|
|
||||||
|
```php |
||||||
|
// src/Domain/Specifications/ItemExpirationSpec.php |
||||||
|
class ItemExpirationSpec |
||||||
|
{ |
||||||
|
public function isExpired(Item $item, DateTimeImmutable $currentTime): bool |
||||||
|
{ |
||||||
|
return $this->getSpec($currentTime)->match($item); |
||||||
|
} |
||||||
|
|
||||||
|
public function getSpec(DateTimeImmutable $currentTime): Specification |
||||||
|
{ |
||||||
|
return new Specification( |
||||||
|
Spec::lte('expirationDate', $currentTime->format('Y-m-d H:i:s')) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Using Specifications with Repositories |
||||||
|
|
||||||
|
### For In-Memory Repositories |
||||||
|
|
||||||
|
```php |
||||||
|
public function findWhere(Specification $specification): array |
||||||
|
{ |
||||||
|
$result = []; |
||||||
|
foreach ($this->items as $item) { |
||||||
|
if ($specification->match($item)) { |
||||||
|
$result[] = $item; |
||||||
|
} |
||||||
|
} |
||||||
|
return $result; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
### For SQL Repositories |
||||||
|
|
||||||
|
The SQL renderer in the example converts specifications to WHERE clauses: |
||||||
|
|
||||||
|
```php |
||||||
|
public function findWhere(Specification $specification): array |
||||||
|
{ |
||||||
|
$params = []; |
||||||
|
$sqlRenderer = new SqlRenderer(); |
||||||
|
$whereClause = $sqlRenderer->render($spec->getSpec(), $params); |
||||||
|
|
||||||
|
$sql = "SELECT * FROM items WHERE $whereClause"; |
||||||
|
$stmt = $this->pdo->prepare($sql); |
||||||
|
$stmt->execute($params); |
||||||
|
|
||||||
|
return $this->mapToItems($stmt->fetchAll(\PDO::FETCH_ASSOC)); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
## Example: SQL Renderer Usage |
||||||
|
|
||||||
|
The commented example in the code shows how to convert a specification to SQL: |
||||||
|
|
||||||
|
```php |
||||||
|
$spec = Spec::and([ |
||||||
|
Spec::eq('status', 'active'), |
||||||
|
Spec::or([ |
||||||
|
Spec::gt('score', 80), |
||||||
|
Spec::in('role', ['admin', 'moderator']) |
||||||
|
]), |
||||||
|
Spec::not(Spec::eq('deleted', true)) |
||||||
|
]); |
||||||
|
|
||||||
|
$params = []; |
||||||
|
$sqlRenderer = new SqlRenderer(); |
||||||
|
$whereClause = $sqlRenderer->render($spec, $params); |
||||||
|
|
||||||
|
// Results in SQL like: |
||||||
|
// (status = ? AND ((score > ?) OR (role IN (?,?))) AND NOT (deleted = ?)) |
||||||
|
// with params: ['active', 80, 'admin', 'moderator', true] |
||||||
|
``` |
||||||
|
|
||||||
|
## Benefits for AutoStore |
||||||
|
|
||||||
|
1. **Consistent business rules**: Item expiration logic is defined once |
||||||
|
2. **Clean code**: No duplicated conditions across repositories, services, etc. |
||||||
|
3. **Efficiency**: Can be used both for in-memory filtering and SQL queries |
||||||
|
4. **Flexibility**: If business rules change (e.g., items expire 3 days after their date), you only update one place |
||||||
@ -0,0 +1,57 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
require_once __DIR__ . '/../vendor/autoload.php'; |
||||||
|
|
||||||
|
use AutoStore\DiContainer; |
||||||
|
use AutoStore\Application\Interfaces\IUserRepository; |
||||||
|
use Psr\Log\LoggerInterface; |
||||||
|
|
||||||
|
try { |
||||||
|
$diContainer = new DiContainer(); |
||||||
|
$logger = $diContainer->get(LoggerInterface::class); |
||||||
|
$userRepository = $diContainer->get(IUserRepository::class); |
||||||
|
|
||||||
|
$logger->info('Starting default users initialization...'); |
||||||
|
|
||||||
|
$requiredUsernames = ['admin', 'user']; |
||||||
|
$missingUsers = []; |
||||||
|
|
||||||
|
foreach ($requiredUsernames as $username) { |
||||||
|
$user = $userRepository->findByUsername($username); |
||||||
|
if ($user === null) { |
||||||
|
$missingUsers[] = $username; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (empty($missingUsers)) { |
||||||
|
$logger->info('Default users already exist. Skipping initialization.'); |
||||||
|
exit(0); |
||||||
|
} |
||||||
|
|
||||||
|
$defaultUsers = [ |
||||||
|
['username' => 'admin', 'password' => 'admin'], |
||||||
|
['username' => 'user', 'password' => 'user'] |
||||||
|
]; |
||||||
|
|
||||||
|
foreach ($defaultUsers as $userData) { |
||||||
|
if (in_array($userData['username'], $missingUsers)) { |
||||||
|
$user = new \AutoStore\Domain\Entities\User( |
||||||
|
uniqid('user_', true), |
||||||
|
$userData['username'], |
||||||
|
password_hash($userData['password'], PASSWORD_DEFAULT) |
||||||
|
); |
||||||
|
$userRepository->save($user); |
||||||
|
$logger->info("Created default user: {$userData['username']}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$logger->info('Default users initialization completed successfully'); |
||||||
|
exit(0); |
||||||
|
|
||||||
|
} catch (\Exception $e) { |
||||||
|
$logger = $logger ?? new \Monolog\Logger('user-init'); |
||||||
|
$logger->error('Default users initialization failed: ' . $e->getMessage()); |
||||||
|
exit(1); |
||||||
|
} |
||||||
@ -1,45 +1,58 @@ |
|||||||
version: "3.9" |
version: "3.9" |
||||||
services: |
services: |
||||||
php: |
app: |
||||||
|
image: php82-app-img |
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/Dockerfile |
dockerfile: docker/Dockerfile |
||||||
image: php82-app-img |
target: php-fpm |
||||||
container_name: php82-app |
container_name: php82-app |
||||||
|
restart: unless-stopped |
||||||
volumes: |
volumes: |
||||||
- ..:/var/www/html |
- ../:/var/www/html:cached |
||||||
networks: |
networks: |
||||||
- app-network |
- app-network |
||||||
|
healthcheck: |
||||||
|
# The container is ready when port 9000 is open |
||||||
|
test: ["CMD", "sh", "-c", "echo | nc -w 5 127.0.0.1 9000"] |
||||||
|
interval: 10s |
||||||
|
timeout: 5s |
||||||
|
retries: 10 |
||||||
|
|
||||||
nginx: |
scheduler: |
||||||
|
image: php82-app-img |
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/nginx.Dockerfile |
dockerfile: docker/Dockerfile |
||||||
image: php82-nginx-img |
target: php-fpm |
||||||
container_name: php82-nginx |
container_name: php82-scheduler |
||||||
ports: |
restart: unless-stopped |
||||||
- "50080:80" |
entrypoint: ["php", "/var/www/html/cli/scheduler.php"] |
||||||
volumes: |
volumes: |
||||||
- ..:/var/www/html |
- ../:/var/www/html:cached |
||||||
depends_on: |
depends_on: |
||||||
- php |
app: |
||||||
|
condition: service_healthy |
||||||
networks: |
networks: |
||||||
- app-network |
- app-network |
||||||
|
|
||||||
scheduler: |
nginx: |
||||||
|
image: php82-nginx-img |
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/Dockerfile |
dockerfile: docker/Dockerfile |
||||||
image: php82-app-img |
target: nginx |
||||||
container_name: php82-scheduler |
container_name: php82-nginx |
||||||
|
restart: unless-stopped |
||||||
|
ports: |
||||||
|
- "50080:80" |
||||||
volumes: |
volumes: |
||||||
- ..:/var/www/html |
- ../:/var/www/html:cached |
||||||
command: ["php", "/var/www/html/cli/scheduler.php"] |
|
||||||
depends_on: |
depends_on: |
||||||
- php |
app: |
||||||
|
condition: service_healthy |
||||||
networks: |
networks: |
||||||
- app-network |
- app-network |
||||||
restart: unless-stopped |
|
||||||
|
|
||||||
networks: |
networks: |
||||||
app-network: |
app-network: |
||||||
|
|||||||
@ -1,5 +1,39 @@ |
|||||||
#!/bin/sh |
#!/bin/sh |
||||||
|
|
||||||
chown -R www-data:www-data /var/www/html |
set -e |
||||||
|
|
||||||
exec "$@" |
# Set group to www-data, but leave owner untouched |
||||||
|
echo "Setting permissions..." |
||||||
|
chgrp -R www-data /var/www/html |
||||||
|
chmod -R g+ws /var/www/html |
||||||
|
|
||||||
|
echo "Installing dependencies..." |
||||||
|
su -s /bin/sh www-data -c "cd /var/www/html && composer install" |
||||||
|
|
||||||
|
echo "Running tests..." |
||||||
|
su -s /bin/sh www-data -c "php vendor/bin/phpunit tests" |
||||||
|
|
||||||
|
echo "Initializing default users..." |
||||||
|
su -s /bin/sh www-data -c "php /var/www/html/cli/initialize-default-users.php" |
||||||
|
|
||||||
|
exec "$@" |
||||||
|
|
||||||
|
|
||||||
|
# #!/bin/sh |
||||||
|
|
||||||
|
# set -e |
||||||
|
|
||||||
|
# echo "Setting permissions..." |
||||||
|
# chgrp -R www-data /var/www/html |
||||||
|
# chmod -R g+ws /var/www/html |
||||||
|
|
||||||
|
# echo "Installing dependencies..." |
||||||
|
# su -s /bin/sh www-data -c "cd /var/www/html && composer install" |
||||||
|
|
||||||
|
# echo "Running tests..." |
||||||
|
# su -s /bin/sh www-data -c "php vendor/bin/phpunit tests" |
||||||
|
|
||||||
|
# echo "Initializing default users..." |
||||||
|
# su -s /bin/sh www-data -c "php /var/www/html/cli/initialize-default-users.php" |
||||||
|
|
||||||
|
# exec su -s /bin/sh www-data -c "$@" |
||||||
@ -1,7 +0,0 @@ |
|||||||
FROM nginx:alpine |
|
||||||
|
|
||||||
COPY docker/default.conf /etc/nginx/conf.d/default.conf |
|
||||||
|
|
||||||
EXPOSE 80 |
|
||||||
|
|
||||||
CMD ["nginx", "-g", "daemon off;"] |
|
||||||
Loading…
Reference in new issue