From 32facc9b6d57b67df7dd99d4a02f5624b4c06663 Mon Sep 17 00:00:00 2001 From: chodak166 Date: Sun, 21 Sep 2025 10:18:46 +0200 Subject: [PATCH] Doc and build cleanup --- .gitignore | 4 - README.md | 18 +- cpp17/docker/Dockerfile | 13 +- golang/PLAN.md | 604 ------------------ golang/SPEC_DETAILS.md | 4 - golang/go.mod | 39 ++ nestjs/docker/Dockerfile | 1 + php8/SPEC_DETAILS.md | 189 ++++++ php8/cli/initialize-default-users.php | 57 ++ php8/cli/scheduler.php | 1 + php8/docker/Dockerfile | 36 +- php8/docker/default.conf | 3 +- php8/docker/docker-compose.yml | 49 +- php8/docker/entrypoint.sh | 38 +- php8/docker/nginx.Dockerfile | 7 - php8/src/Application.php | 7 - php8/src/DiContainer.php | 4 +- .../Domain/Specifications/Specification.php | 17 +- .../Repositories/FileItemRepository.php | 2 +- .../Repositories/FileUserRepository.php | 2 +- testing/tavern/export.sh | 3 +- 21 files changed, 422 insertions(+), 676 deletions(-) delete mode 100644 golang/PLAN.md create mode 100644 golang/go.mod create mode 100644 php8/SPEC_DETAILS.md create mode 100644 php8/cli/initialize-default-users.php delete mode 100755 php8/docker/nginx.Dockerfile diff --git a/.gitignore b/.gitignore index a75f195..d519d3d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,10 +21,6 @@ tmp *.dylib *.dll -# Fortran module files -*.mod -*.smod - # Compiled Static libraries *.lai *.la diff --git a/README.md b/README.md index 3d89881..679cad5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This repository hosts multiple implementations of the same back-end application. Following principles such as **SOLID** and maintainable architectural patterns (**Clean, Hexagonal, Onion, or even DDD**) is recommended to clearly showcase the strengths and idioms of each technology. -Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or strict CQRS unless intentionally focusing on those patterns for comparison). +Some over-engineering is acceptable to demonstrate architectural features, but please keep implementations readable and avoid excessive complexity (e.g., skip event sourcing or atomic transactions unless intentionally focusing on those patterns for comparison). --- @@ -113,18 +113,20 @@ AutoStore/ Business rules like expiration checks (`expirationDate <= currentDate`) represent domain knowledge that **must have a single source of truth**. This logic might evolve (e.g., to `<= currentDate - N days` or vary by item type) and should never be duplicated across the codebase. -While simple predicates (like `findWhere(predicate(Item))`) work for in-memory repositories, SQL-based repositories need to translate these rules into efficient WHERE clauses. To solve this, consider implementing a specification pattern with a simple condition abstraction that exposes the business rule as composable conditions (field, operator, value). +While simple predicates (like `findWhere(predicate(Item))`) work for in-memory repositories, SQL-based repositories need to translate these rules into efficient WHERE clauses. One solution (not too over-engineered) would be to pass DTO or value object ready to be put into queries (e.g., `fetchExpiredItems(calculatedConditionFields)`). -This allows domain services to define rules once, use cases to apply them consistently, and repositories to translate them into optimal queries by interpreting the conditions according to their storage mechanism. Avoid duplicating the business logic in repository implementations - instead, let repositories consume the specification and build their queries accordingly. +But for fun and possible UI search, consider implementing a specification pattern with a simple condition abstraction that exposes the business rule as composable conditions (field, operator, value). + +This allows domain services to define rules once, use cases to apply them consistently, and repositories to translate them into optimal queries by interpreting the conditions according to their storage mechanism. Avoid duplicating the business logic in repository implementations - instead, let repositories consume the specification and build their queries accordingly. This aims to overcome to-repo-and-back drawbacks depicted in *Evans, Eric (2003). Domain Driven Design. Final Manuscript*. --- ## Build and Run -Ideally, each implementation should include a `/docker/docker-compose.yml` file so that you can simply run: +Each implementation should include a `/docker/docker-compose.yml` file so that you can simply run: ```bash -docker compose up --build +cd docker && docker compose up --build ``` to build, test and run the application. @@ -146,3 +148,9 @@ Here's a summary of example API endpoints: Suggested base URL is `http://localhost:50080/api/v1/`. +## Testing + +- Each implementation should include its own **unit tests** and **integration tests**, which must run automatically during the Docker image build. +- Implementation-independent functional tests are provided in `testing/tavern/` +- Tavern API tests (requests and assertions) must pass for every implementation to ensure consistent behavior across all technology stacks. +- For debugging and verifying the automatic ordering feature, use the helper service in `testing/http-echo-server/` which provides a simple Docker Compose setup that listens on port 8888 and logs all incoming POST requests, allowing you to observe when expired items trigger order notifications. \ No newline at end of file diff --git a/cpp17/docker/Dockerfile b/cpp17/docker/Dockerfile index 928480b..bbf2e9f 100644 --- a/cpp17/docker/Dockerfile +++ b/cpp17/docker/Dockerfile @@ -2,20 +2,27 @@ FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder WORKDIR /workspace +COPY ../CMakeLists.txt . +COPY ../vcpkg.json . + +RUN vcpkg install + +# Cche stays valid if only code changes COPY .. . -# generate and build RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ -H/workspace -B/workspace/build -G Ninja RUN cmake --build /workspace/build --config Release --target all -j 8 -- -RUN cd /workspace/build && ctest --output-on-failure . +# run tests +RUN cd /workspace/build && ctest --output-on-failure . FROM ubuntu:24.04 AS runtime WORKDIR /app COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore +COPY --from=builder /workspace/build/bin/data ./data -CMD ["./AutoStore"] +CMD ["./AutoStore"] \ No newline at end of file diff --git a/golang/PLAN.md b/golang/PLAN.md deleted file mode 100644 index 5522153..0000000 --- a/golang/PLAN.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/golang/SPEC_DETAILS.md b/golang/SPEC_DETAILS.md index f6964fb..0a768d9 100644 --- a/golang/SPEC_DETAILS.md +++ b/golang/SPEC_DETAILS.md @@ -1,9 +1,5 @@ # Specification Pattern Implementation -This document explains the Specification pattern implementation for building dynamic queries and conditions that can be used across different layers of the application. - -## Overview - The Specification pattern allows you to encapsulate business logic for filtering and querying data. Instead of writing SQL queries directly, you build specifications using Go code that can later be converted to SQL, used for in-memory filtering, or other purposes. ## Core Components diff --git a/golang/go.mod b/golang/go.mod new file mode 100644 index 0000000..f90768e --- /dev/null +++ b/golang/go.mod @@ -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 +) diff --git a/nestjs/docker/Dockerfile b/nestjs/docker/Dockerfile index 23aa9aa..acbc823 100644 --- a/nestjs/docker/Dockerfile +++ b/nestjs/docker/Dockerfile @@ -11,6 +11,7 @@ RUN npm install COPY src ./src RUN npm run build +RUN npm test FROM node:24.0.1-alpine diff --git a/php8/SPEC_DETAILS.md b/php8/SPEC_DETAILS.md new file mode 100644 index 0000000..9764d16 --- /dev/null +++ b/php8/SPEC_DETAILS.md @@ -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 diff --git a/php8/cli/initialize-default-users.php b/php8/cli/initialize-default-users.php new file mode 100644 index 0000000..e9dc524 --- /dev/null +++ b/php8/cli/initialize-default-users.php @@ -0,0 +1,57 @@ +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); +} \ No newline at end of file diff --git a/php8/cli/scheduler.php b/php8/cli/scheduler.php index 283d92a..2d7ac13 100755 --- a/php8/cli/scheduler.php +++ b/php8/cli/scheduler.php @@ -116,6 +116,7 @@ class Scheduler // Main execution try { + echo "Scheduler started\n"; $diContainer = new DiContainer(); $logger = $diContainer->get(LoggerInterface::class); $tasksDirectory = __DIR__ . '/scheduler-tasks'; diff --git a/php8/docker/Dockerfile b/php8/docker/Dockerfile index 5d7f9f7..34bf69b 100755 --- a/php8/docker/Dockerfile +++ b/php8/docker/Dockerfile @@ -1,6 +1,5 @@ -FROM php:8.2-fpm-alpine +FROM php:8.2-fpm-alpine AS base -# Install system dependencies RUN apk add --no-cache \ icu-dev \ libzip-dev \ @@ -16,20 +15,41 @@ RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-configure pcntl --enable-pcntl \ && docker-php-ext-install pcntl -# Create PHP configuration file RUN echo "memory_limit = 256M" > /usr/local/etc/php/conf.d/custom.ini \ && echo "upload_max_filesize = 100M" >> /usr/local/etc/php/conf.d/custom.ini \ && echo "post_max_size = 100M" >> /usr/local/etc/php/conf.d/custom.ini \ - && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/custom.ini + && echo "max_execution_time = 300" >> /usr/local/etc/php/conf.d/custom.ini \ + && echo "user = www-data" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "group = www-data" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "listen.owner = www-data" >> /usr/local/etc/php-fpm.d/www.conf \ + && echo "listen.group = www-data" >> /usr/local/etc/php-fpm.d/www.conf -# Set working directory WORKDIR /var/www/html -# Expose port 9000 for PHP-FPM -EXPOSE 9000 +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + + +########################## +# PHP-FPM stage +FROM base AS php-fpm COPY docker/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh + +# Expose port 9000 for PHP-FPM +EXPOSE 9000 + ENTRYPOINT [ "/entrypoint.sh" ] -CMD ["php-fpm"] \ No newline at end of file +CMD ["php-fpm"] + + +########################## +# Nginx stage - final nginx image +FROM nginx:alpine AS nginx + +COPY docker/default.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/php8/docker/default.conf b/php8/docker/default.conf index 88e89d4..eef4827 100755 --- a/php8/docker/default.conf +++ b/php8/docker/default.conf @@ -1,6 +1,5 @@ server { listen 80; - server_name localhost; root /var/www/html; index index.php index.html; @@ -11,7 +10,7 @@ server { location ~ \.php$ { try_files $uri =404; fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass php:9000; + fastcgi_pass app:9000; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; diff --git a/php8/docker/docker-compose.yml b/php8/docker/docker-compose.yml index 361211b..3ffdf1a 100755 --- a/php8/docker/docker-compose.yml +++ b/php8/docker/docker-compose.yml @@ -1,45 +1,58 @@ version: "3.9" services: - php: + app: + image: php82-app-img build: context: .. dockerfile: docker/Dockerfile - image: php82-app-img + target: php-fpm container_name: php82-app + restart: unless-stopped volumes: - - ..:/var/www/html + - ../:/var/www/html:cached networks: - 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: context: .. - dockerfile: docker/nginx.Dockerfile - image: php82-nginx-img - container_name: php82-nginx - ports: - - "50080:80" + dockerfile: docker/Dockerfile + target: php-fpm + container_name: php82-scheduler + restart: unless-stopped + entrypoint: ["php", "/var/www/html/cli/scheduler.php"] volumes: - - ..:/var/www/html + - ../:/var/www/html:cached depends_on: - - php + app: + condition: service_healthy networks: - app-network - scheduler: + nginx: + image: php82-nginx-img build: context: .. dockerfile: docker/Dockerfile - image: php82-app-img - container_name: php82-scheduler + target: nginx + container_name: php82-nginx + restart: unless-stopped + ports: + - "50080:80" volumes: - - ..:/var/www/html - command: ["php", "/var/www/html/cli/scheduler.php"] + - ../:/var/www/html:cached depends_on: - - php + app: + condition: service_healthy networks: - app-network - restart: unless-stopped networks: app-network: diff --git a/php8/docker/entrypoint.sh b/php8/docker/entrypoint.sh index 169a28e..814f997 100644 --- a/php8/docker/entrypoint.sh +++ b/php8/docker/entrypoint.sh @@ -1,5 +1,39 @@ #!/bin/sh -chown -R www-data:www-data /var/www/html +set -e -exec "$@" \ No newline at end of file +# 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 "$@" \ No newline at end of file diff --git a/php8/docker/nginx.Dockerfile b/php8/docker/nginx.Dockerfile deleted file mode 100755 index 7d69fed..0000000 --- a/php8/docker/nginx.Dockerfile +++ /dev/null @@ -1,7 +0,0 @@ -FROM nginx:alpine - -COPY docker/default.conf /etc/nginx/conf.d/default.conf - -EXPOSE 80 - -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/php8/src/Application.php b/php8/src/Application.php index 6023b26..e3d5bb4 100755 --- a/php8/src/Application.php +++ b/php8/src/Application.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace AutoStore; -use AutoStore\Application\Services\UserInitializationService; use AutoStore\WebApi\Router; use Slim\Factory\AppFactory; @@ -18,7 +17,6 @@ class Application $this->di = new DiContainer(); $this->app = AppFactory::create(); - $this->initializeDefaultUsers(); $this->setupMiddleware(); $this->setupRoutes(); } @@ -36,11 +34,6 @@ class Application $router->setupRoutes(); } - private function initializeDefaultUsers(): void - { - $userInitializationService = $this->di->get(UserInitializationService::class); - $userInitializationService->createDefaultUsers(); - } public function run(): void { diff --git a/php8/src/DiContainer.php b/php8/src/DiContainer.php index 06a1b6d..6d5097c 100755 --- a/php8/src/DiContainer.php +++ b/php8/src/DiContainer.php @@ -43,7 +43,9 @@ class DiContainer // Simplified app config, for real app use settings repository (env variables, database, etc.) $configJson = file_get_contents(self::ROOT_DIR . '/configuration.json'); $config = json_decode($configJson, true); - $this->storagePath = $config['storage_directory'] ?? self::ROOT_DIR . '/storage'; + $storageDir = $config['storage_directory'] ?? './storage'; + // Convert relative path to absolute path + $this->storagePath = $storageDir === './storage' ? self::ROOT_DIR . '/storage' : $storageDir; $this->jwtSecret = $config['jwt_secret'] ?? 'secret-key'; $this->diContainer = new Container(); diff --git a/php8/src/Domain/Specifications/Specification.php b/php8/src/Domain/Specifications/Specification.php index f54f72d..2a71118 100644 --- a/php8/src/Domain/Specifications/Specification.php +++ b/php8/src/Domain/Specifications/Specification.php @@ -194,14 +194,15 @@ class Spec // ----------------- // Example usage -$spec = Spec::and([ - Spec::eq('status', 'active'), - Spec::or([ - Spec::gt('score', 80), - Spec::in('role', ['admin', 'moderator']) - ]), - Spec::not(Spec::eq('deleted', true)) -]); +$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(); diff --git a/php8/src/Infrastructure/Repositories/FileItemRepository.php b/php8/src/Infrastructure/Repositories/FileItemRepository.php index af45ef5..54192ab 100755 --- a/php8/src/Infrastructure/Repositories/FileItemRepository.php +++ b/php8/src/Infrastructure/Repositories/FileItemRepository.php @@ -92,7 +92,7 @@ class FileItemRepository implements IItemRepository private function ensureStorageDirectoryExists(): void { if (!is_dir($this->storagePath)) { - if (!mkdir($this->storagePath, 0755, true)) { + if (!mkdir($this->storagePath, 0775, true)) { throw new ApplicationException("Failed to create storage directory: {$this->storagePath}"); } } diff --git a/php8/src/Infrastructure/Repositories/FileUserRepository.php b/php8/src/Infrastructure/Repositories/FileUserRepository.php index 727d247..d1c1761 100755 --- a/php8/src/Infrastructure/Repositories/FileUserRepository.php +++ b/php8/src/Infrastructure/Repositories/FileUserRepository.php @@ -72,7 +72,7 @@ class FileUserRepository implements IUserRepository private function ensureStorageDirectoryExists(): void { if (!is_dir($this->storagePath)) { - if (!mkdir($this->storagePath, 0755, true)) { + if (!mkdir($this->storagePath, 0775, true)) { throw new ApplicationException("Failed to create storage directory: {$this->storagePath}"); } } diff --git a/testing/tavern/export.sh b/testing/tavern/export.sh index ad4373d..fa43bb6 100644 --- a/testing/tavern/export.sh +++ b/testing/tavern/export.sh @@ -10,7 +10,8 @@ fi export TEST_SERVER_ADDRESS="127.0.0.1" export TEST_SERVER_PORT="50080" export TEST_API_BASE="api/v1" -export TEST_ORDER_URL="http://192.168.20.2:8888/" +# export TEST_ORDER_URL="http://192.168.20.2:8888/" +export TEST_ORDER_URL="http://host.docker.internal:8888/" export TEST_USER1_ID="1000" export TEST_USER1_LOGIN="admin"