Compare commits
No commits in common. 'master' and 'golang-init' have entirely different histories.
master
...
golang-ini
21 changed files with 676 additions and 422 deletions
@ -0,0 +1,604 @@ |
|||||||
|
# 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. |
||||||
@ -1,39 +0,0 @@ |
|||||||
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 |
|
||||||
) |
|
||||||
@ -1,189 +0,0 @@ |
|||||||
# 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 |
|
||||||
@ -1,57 +0,0 @@ |
|||||||
<?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,58 +1,45 @@ |
|||||||
version: "3.9" |
version: "3.9" |
||||||
services: |
services: |
||||||
app: |
php: |
||||||
image: php82-app-img |
|
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/Dockerfile |
dockerfile: docker/Dockerfile |
||||||
target: php-fpm |
image: php82-app-img |
||||||
container_name: php82-app |
container_name: php82-app |
||||||
restart: unless-stopped |
|
||||||
volumes: |
volumes: |
||||||
- ../:/var/www/html:cached |
- ..:/var/www/html |
||||||
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 |
|
||||||
|
|
||||||
scheduler: |
nginx: |
||||||
image: php82-app-img |
|
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/Dockerfile |
dockerfile: docker/nginx.Dockerfile |
||||||
target: php-fpm |
image: php82-nginx-img |
||||||
container_name: php82-scheduler |
container_name: php82-nginx |
||||||
restart: unless-stopped |
ports: |
||||||
entrypoint: ["php", "/var/www/html/cli/scheduler.php"] |
- "50080:80" |
||||||
volumes: |
volumes: |
||||||
- ../:/var/www/html:cached |
- ..:/var/www/html |
||||||
depends_on: |
depends_on: |
||||||
app: |
- php |
||||||
condition: service_healthy |
|
||||||
networks: |
networks: |
||||||
- app-network |
- app-network |
||||||
|
|
||||||
nginx: |
scheduler: |
||||||
image: php82-nginx-img |
|
||||||
build: |
build: |
||||||
context: .. |
context: .. |
||||||
dockerfile: docker/Dockerfile |
dockerfile: docker/Dockerfile |
||||||
target: nginx |
image: php82-app-img |
||||||
container_name: php82-nginx |
container_name: php82-scheduler |
||||||
restart: unless-stopped |
|
||||||
ports: |
|
||||||
- "50080:80" |
|
||||||
volumes: |
volumes: |
||||||
- ../:/var/www/html:cached |
- ..:/var/www/html |
||||||
|
command: ["php", "/var/www/html/cli/scheduler.php"] |
||||||
depends_on: |
depends_on: |
||||||
app: |
- php |
||||||
condition: service_healthy |
|
||||||
networks: |
networks: |
||||||
- app-network |
- app-network |
||||||
|
restart: unless-stopped |
||||||
|
|
||||||
networks: |
networks: |
||||||
app-network: |
app-network: |
||||||
|
|||||||
@ -1,39 +1,5 @@ |
|||||||
#!/bin/sh |
#!/bin/sh |
||||||
|
|
||||||
set -e |
chown -R www-data:www-data /var/www/html |
||||||
|
|
||||||
# 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 "$@" |
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 "$@" |
|
||||||
@ -0,0 +1,7 @@ |
|||||||
|
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