Browse Source

Doc and build cleanup

master tested/20250921
chodak166 3 months ago
parent
commit
32facc9b6d
  1. 4
      .gitignore
  2. 18
      README.md
  3. 13
      cpp17/docker/Dockerfile
  4. 604
      golang/PLAN.md
  5. 4
      golang/SPEC_DETAILS.md
  6. 39
      golang/go.mod
  7. 1
      nestjs/docker/Dockerfile
  8. 189
      php8/SPEC_DETAILS.md
  9. 57
      php8/cli/initialize-default-users.php
  10. 1
      php8/cli/scheduler.php
  11. 36
      php8/docker/Dockerfile
  12. 3
      php8/docker/default.conf
  13. 49
      php8/docker/docker-compose.yml
  14. 38
      php8/docker/entrypoint.sh
  15. 7
      php8/docker/nginx.Dockerfile
  16. 7
      php8/src/Application.php
  17. 4
      php8/src/DiContainer.php
  18. 17
      php8/src/Domain/Specifications/Specification.php
  19. 2
      php8/src/Infrastructure/Repositories/FileItemRepository.php
  20. 2
      php8/src/Infrastructure/Repositories/FileUserRepository.php
  21. 3
      testing/tavern/export.sh

4
.gitignore vendored

@ -21,10 +21,6 @@ tmp
*.dylib *.dylib
*.dll *.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries # Compiled Static libraries
*.lai *.lai
*.la *.la

18
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. 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. 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<bool>(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<bool>(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 ## Build and Run
Ideally, each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run: Each implementation should include a `<impl>/docker/docker-compose.yml` file so that you can simply run:
```bash ```bash
docker compose up --build cd docker && docker compose up --build
``` ```
to build, test and run the application. 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/`. 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.

13
cpp17/docker/Dockerfile

@ -2,20 +2,27 @@ FROM kuyoh/vcpkg:2025.06.13-ubuntu24.04 AS builder
WORKDIR /workspace WORKDIR /workspace
COPY ../CMakeLists.txt .
COPY ../vcpkg.json .
RUN vcpkg install
# Cche stays valid if only code changes
COPY .. . COPY .. .
# generate and build
RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ RUN cmake -DCMAKE_TOOLCHAIN_FILE:STRING=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \
-DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \ -DCMAKE_EXPORT_COMPILE_COMMANDS:BOOL=TRUE -DCMAKE_BUILD_TYPE:STRING=Release \
-H/workspace -B/workspace/build -G Ninja -H/workspace -B/workspace/build -G Ninja
RUN cmake --build /workspace/build --config Release --target all -j 8 -- 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 FROM ubuntu:24.04 AS runtime
WORKDIR /app WORKDIR /app
COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore COPY --from=builder /workspace/build/bin/AutoStore ./AutoStore
COPY --from=builder /workspace/build/bin/data ./data
CMD ["./AutoStore"] CMD ["./AutoStore"]

604
golang/PLAN.md

@ -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.

4
golang/SPEC_DETAILS.md

@ -1,9 +1,5 @@
# Specification Pattern Implementation # 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. 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 ## Core Components

39
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
)

1
nestjs/docker/Dockerfile

@ -11,6 +11,7 @@ RUN npm install
COPY src ./src COPY src ./src
RUN npm run build RUN npm run build
RUN npm test
FROM node:24.0.1-alpine FROM node:24.0.1-alpine

189
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

57
php8/cli/initialize-default-users.php

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use AutoStore\DiContainer;
use AutoStore\Application\Interfaces\IUserRepository;
use Psr\Log\LoggerInterface;
try {
$diContainer = new DiContainer();
$logger = $diContainer->get(LoggerInterface::class);
$userRepository = $diContainer->get(IUserRepository::class);
$logger->info('Starting default users initialization...');
$requiredUsernames = ['admin', 'user'];
$missingUsers = [];
foreach ($requiredUsernames as $username) {
$user = $userRepository->findByUsername($username);
if ($user === null) {
$missingUsers[] = $username;
}
}
if (empty($missingUsers)) {
$logger->info('Default users already exist. Skipping initialization.');
exit(0);
}
$defaultUsers = [
['username' => 'admin', 'password' => 'admin'],
['username' => 'user', 'password' => 'user']
];
foreach ($defaultUsers as $userData) {
if (in_array($userData['username'], $missingUsers)) {
$user = new \AutoStore\Domain\Entities\User(
uniqid('user_', true),
$userData['username'],
password_hash($userData['password'], PASSWORD_DEFAULT)
);
$userRepository->save($user);
$logger->info("Created default user: {$userData['username']}");
}
}
$logger->info('Default users initialization completed successfully');
exit(0);
} catch (\Exception $e) {
$logger = $logger ?? new \Monolog\Logger('user-init');
$logger->error('Default users initialization failed: ' . $e->getMessage());
exit(1);
}

1
php8/cli/scheduler.php

@ -116,6 +116,7 @@ class Scheduler
// Main execution // Main execution
try { try {
echo "Scheduler started\n";
$diContainer = new DiContainer(); $diContainer = new DiContainer();
$logger = $diContainer->get(LoggerInterface::class); $logger = $diContainer->get(LoggerInterface::class);
$tasksDirectory = __DIR__ . '/scheduler-tasks'; $tasksDirectory = __DIR__ . '/scheduler-tasks';

36
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 \ RUN apk add --no-cache \
icu-dev \ icu-dev \
libzip-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-configure pcntl --enable-pcntl \
&& docker-php-ext-install pcntl && docker-php-ext-install pcntl
# Create PHP configuration file
RUN echo "memory_limit = 256M" > /usr/local/etc/php/conf.d/custom.ini \ 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 "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 "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 WORKDIR /var/www/html
# Expose port 9000 for PHP-FPM COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
EXPOSE 9000
##########################
# PHP-FPM stage
FROM base AS php-fpm
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh RUN chmod +x /entrypoint.sh
# Expose port 9000 for PHP-FPM
EXPOSE 9000
ENTRYPOINT [ "/entrypoint.sh" ] ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["php-fpm"] 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;"]

3
php8/docker/default.conf

@ -1,6 +1,5 @@
server { server {
listen 80; listen 80;
server_name localhost;
root /var/www/html; root /var/www/html;
index index.php index.html; index index.php index.html;
@ -11,7 +10,7 @@ server {
location ~ \.php$ { location ~ \.php$ {
try_files $uri =404; try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass php:9000; fastcgi_pass app:9000;
fastcgi_index index.php; fastcgi_index index.php;
include fastcgi_params; include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

49
php8/docker/docker-compose.yml

@ -1,45 +1,58 @@
version: "3.9" version: "3.9"
services: services:
php: app:
image: php82-app-img
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
image: php82-app-img target: php-fpm
container_name: php82-app container_name: php82-app
restart: unless-stopped
volumes: volumes:
- ..:/var/www/html - ../:/var/www/html:cached
networks: networks:
- app-network - app-network
healthcheck:
# The container is ready when port 9000 is open
test: ["CMD", "sh", "-c", "echo | nc -w 5 127.0.0.1 9000"]
interval: 10s
timeout: 5s
retries: 10
nginx: scheduler:
image: php82-app-img
build: build:
context: .. context: ..
dockerfile: docker/nginx.Dockerfile dockerfile: docker/Dockerfile
image: php82-nginx-img target: php-fpm
container_name: php82-nginx container_name: php82-scheduler
ports: restart: unless-stopped
- "50080:80" entrypoint: ["php", "/var/www/html/cli/scheduler.php"]
volumes: volumes:
- ..:/var/www/html - ../:/var/www/html:cached
depends_on: depends_on:
- php app:
condition: service_healthy
networks: networks:
- app-network - app-network
scheduler: nginx:
image: php82-nginx-img
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
image: php82-app-img target: nginx
container_name: php82-scheduler container_name: php82-nginx
restart: unless-stopped
ports:
- "50080:80"
volumes: volumes:
- ..:/var/www/html - ../:/var/www/html:cached
command: ["php", "/var/www/html/cli/scheduler.php"]
depends_on: depends_on:
- php app:
condition: service_healthy
networks: networks:
- app-network - app-network
restart: unless-stopped
networks: networks:
app-network: app-network:

38
php8/docker/entrypoint.sh

@ -1,5 +1,39 @@
#!/bin/sh #!/bin/sh
chown -R www-data:www-data /var/www/html set -e
exec "$@" # Set group to www-data, but leave owner untouched
echo "Setting permissions..."
chgrp -R www-data /var/www/html
chmod -R g+ws /var/www/html
echo "Installing dependencies..."
su -s /bin/sh www-data -c "cd /var/www/html && composer install"
echo "Running tests..."
su -s /bin/sh www-data -c "php vendor/bin/phpunit tests"
echo "Initializing default users..."
su -s /bin/sh www-data -c "php /var/www/html/cli/initialize-default-users.php"
exec "$@"
# #!/bin/sh
# set -e
# echo "Setting permissions..."
# chgrp -R www-data /var/www/html
# chmod -R g+ws /var/www/html
# echo "Installing dependencies..."
# su -s /bin/sh www-data -c "cd /var/www/html && composer install"
# echo "Running tests..."
# su -s /bin/sh www-data -c "php vendor/bin/phpunit tests"
# echo "Initializing default users..."
# su -s /bin/sh www-data -c "php /var/www/html/cli/initialize-default-users.php"
# exec su -s /bin/sh www-data -c "$@"

7
php8/docker/nginx.Dockerfile

@ -1,7 +0,0 @@
FROM nginx:alpine
COPY docker/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

7
php8/src/Application.php

@ -4,7 +4,6 @@ declare(strict_types=1);
namespace AutoStore; namespace AutoStore;
use AutoStore\Application\Services\UserInitializationService;
use AutoStore\WebApi\Router; use AutoStore\WebApi\Router;
use Slim\Factory\AppFactory; use Slim\Factory\AppFactory;
@ -18,7 +17,6 @@ class Application
$this->di = new DiContainer(); $this->di = new DiContainer();
$this->app = AppFactory::create(); $this->app = AppFactory::create();
$this->initializeDefaultUsers();
$this->setupMiddleware(); $this->setupMiddleware();
$this->setupRoutes(); $this->setupRoutes();
} }
@ -36,11 +34,6 @@ class Application
$router->setupRoutes(); $router->setupRoutes();
} }
private function initializeDefaultUsers(): void
{
$userInitializationService = $this->di->get(UserInitializationService::class);
$userInitializationService->createDefaultUsers();
}
public function run(): void public function run(): void
{ {

4
php8/src/DiContainer.php

@ -43,7 +43,9 @@ class DiContainer
// Simplified app config, for real app use settings repository (env variables, database, etc.) // Simplified app config, for real app use settings repository (env variables, database, etc.)
$configJson = file_get_contents(self::ROOT_DIR . '/configuration.json'); $configJson = file_get_contents(self::ROOT_DIR . '/configuration.json');
$config = json_decode($configJson, true); $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->jwtSecret = $config['jwt_secret'] ?? 'secret-key';
$this->diContainer = new Container(); $this->diContainer = new Container();

17
php8/src/Domain/Specifications/Specification.php

@ -194,14 +194,15 @@ class Spec
// ----------------- // -----------------
// Example usage // Example usage
$spec = Spec::and([ $spec =
Spec::eq('status', 'active'), Spec::and([
Spec::or([ Spec::eq('status', 'active'),
Spec::gt('score', 80), Spec::or([
Spec::in('role', ['admin', 'moderator']) Spec::gt('score', 80),
]), Spec::in('role', ['admin', 'moderator'])
Spec::not(Spec::eq('deleted', true)) ]),
]); Spec::not(Spec::eq('deleted', true))
]);
$params = []; $params = [];
$sqlRenderer = new SqlRenderer(); $sqlRenderer = new SqlRenderer();

2
php8/src/Infrastructure/Repositories/FileItemRepository.php

@ -92,7 +92,7 @@ class FileItemRepository implements IItemRepository
private function ensureStorageDirectoryExists(): void private function ensureStorageDirectoryExists(): void
{ {
if (!is_dir($this->storagePath)) { 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}"); throw new ApplicationException("Failed to create storage directory: {$this->storagePath}");
} }
} }

2
php8/src/Infrastructure/Repositories/FileUserRepository.php

@ -72,7 +72,7 @@ class FileUserRepository implements IUserRepository
private function ensureStorageDirectoryExists(): void private function ensureStorageDirectoryExists(): void
{ {
if (!is_dir($this->storagePath)) { 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}"); throw new ApplicationException("Failed to create storage directory: {$this->storagePath}");
} }
} }

3
testing/tavern/export.sh

@ -10,7 +10,8 @@ fi
export TEST_SERVER_ADDRESS="127.0.0.1" export TEST_SERVER_ADDRESS="127.0.0.1"
export TEST_SERVER_PORT="50080" export TEST_SERVER_PORT="50080"
export TEST_API_BASE="api/v1" 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_ID="1000"
export TEST_USER1_LOGIN="admin" export TEST_USER1_LOGIN="admin"

Loading…
Cancel
Save