41 changed files with 11174 additions and 557 deletions
@ -0,0 +1,39 @@ |
|||||||
|
FROM node:24.0.1-alpine |
||||||
|
|
||||||
|
WORKDIR /usr/src/app |
||||||
|
|
||||||
|
# Install system dependencies |
||||||
|
RUN apk add --no-cache \ |
||||||
|
git \ |
||||||
|
bash \ |
||||||
|
curl \ |
||||||
|
sudo |
||||||
|
|
||||||
|
# Give sudo permissions to the developer user |
||||||
|
RUN echo '%developer ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/developer && \ |
||||||
|
chmod 0440 /etc/sudoers.d/developer |
||||||
|
|
||||||
|
# Configure user permissions |
||||||
|
ARG USER_ID=1000 |
||||||
|
ARG GROUP_ID=1000 |
||||||
|
|
||||||
|
# Create a user with matching UID/GID |
||||||
|
RUN if ! getent group $GROUP_ID > /dev/null 2>&1; then \ |
||||||
|
addgroup -g $GROUP_ID developer; \ |
||||||
|
else \ |
||||||
|
addgroup developer; \ |
||||||
|
fi && \ |
||||||
|
if ! getent passwd $USER_ID > /dev/null 2>&1; then \ |
||||||
|
adduser -D -u $USER_ID -G developer -s /bin/sh developer; \ |
||||||
|
else \ |
||||||
|
adduser -D -G developer -s /bin/sh developer; \ |
||||||
|
fi |
||||||
|
|
||||||
|
RUN chown -R $USER_ID:$GROUP_ID /usr/src/app |
||||||
|
|
||||||
|
USER $USER_ID:$GROUP_ID |
||||||
|
|
||||||
|
# Expose port 3000 for NestJS |
||||||
|
EXPOSE 3000 |
||||||
|
|
||||||
|
CMD ["npm", "run", "start:dev"] |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
{ |
||||||
|
"name": "NestJS dev container", |
||||||
|
"dockerComposeFile": "./docker-compose.yml", |
||||||
|
"service": "app", |
||||||
|
"workspaceFolder": "/usr/src/app", |
||||||
|
"customizations": { |
||||||
|
"vscode": { |
||||||
|
"settings": { |
||||||
|
"terminal.integrated.defaultProfile.linux": "bash", |
||||||
|
"node.js.version": "24.0.1" |
||||||
|
}, |
||||||
|
"extensions": [ |
||||||
|
"ms-vscode.vscode-typescript-next", |
||||||
|
"ms-nodejs.vscode-node-debug2", |
||||||
|
"ms-vscode.vscode-json", |
||||||
|
"esbenp.prettier-vscode", |
||||||
|
"dbaeumer.vscode-eslint", |
||||||
|
"christian-kohler.npm-intellisense", |
||||||
|
"christian-kohler.path-intellisense" |
||||||
|
] |
||||||
|
} |
||||||
|
}, |
||||||
|
"forwardPorts": [3000], |
||||||
|
"remoteUser": "developer", |
||||||
|
"postCreateCommand": "sudo chown -R developer:developer /usr/src/app && npm install" |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
version: "3.9" |
||||||
|
services: |
||||||
|
app: |
||||||
|
build: |
||||||
|
context: .. |
||||||
|
dockerfile: .devcontainer/Dockerfile |
||||||
|
args: |
||||||
|
USER_ID: ${USER_ID:-1000} |
||||||
|
GROUP_ID: ${GROUP_ID:-1000} |
||||||
|
image: dev-nestjs-img |
||||||
|
container_name: dev-nestjs |
||||||
|
user: "developer" |
||||||
|
volumes: |
||||||
|
- ../:/usr/src/app:cached |
||||||
|
- node_modules:/usr/src/app/node_modules |
||||||
|
environment: |
||||||
|
NODE_ENV: development |
||||||
|
ports: |
||||||
|
- "50080:3000" |
||||||
|
networks: |
||||||
|
- dev-network |
||||||
|
command: sleep infinity |
||||||
|
|
||||||
|
volumes: |
||||||
|
node_modules: |
||||||
|
|
||||||
|
networks: |
||||||
|
dev-network: |
||||||
|
driver: bridge |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
FROM node:24.0.1-alpine as builder |
||||||
|
|
||||||
|
WORKDIR /app |
||||||
|
|
||||||
|
COPY package*.json ./ |
||||||
|
COPY nest-cli.json ./ |
||||||
|
COPY tsconfig.json ./ |
||||||
|
|
||||||
|
RUN npm install |
||||||
|
|
||||||
|
COPY src ./src |
||||||
|
|
||||||
|
RUN npm run build |
||||||
|
|
||||||
|
|
||||||
|
FROM node:24.0.1-alpine |
||||||
|
|
||||||
|
WORKDIR /app |
||||||
|
|
||||||
|
COPY package*.json ./ |
||||||
|
|
||||||
|
RUN npm install --only=production |
||||||
|
|
||||||
|
COPY --from=builder /app/dist ./dist |
||||||
|
|
||||||
|
EXPOSE 3000 |
||||||
|
|
||||||
|
CMD [ "node", "dist/main" ] |
||||||
@ -0,0 +1,17 @@ |
|||||||
|
version: "3.9" |
||||||
|
services: |
||||||
|
app: |
||||||
|
build: |
||||||
|
context: .. |
||||||
|
dockerfile: docker/Dockerfile |
||||||
|
image: nestjs-app-img |
||||||
|
container_name: nestjs-app |
||||||
|
ports: |
||||||
|
- "50080:3000" |
||||||
|
networks: |
||||||
|
- app-network |
||||||
|
restart: unless-stopped |
||||||
|
|
||||||
|
networks: |
||||||
|
app-network: |
||||||
|
driver: bridge |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
{ |
||||||
|
"$schema": "https://json.schemastore.org/nest-cli", |
||||||
|
"collection": "@nestjs/schematics", |
||||||
|
"sourceRoot": "src", |
||||||
|
"compilerOptions": { |
||||||
|
"deleteOutDir": true |
||||||
|
} |
||||||
|
} |
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,70 @@ |
|||||||
|
{ |
||||||
|
"name": "autostore-nestjs", |
||||||
|
"version": "0.0.1", |
||||||
|
"description": "AutoStore implementation with NestJS", |
||||||
|
"author": "", |
||||||
|
"license": "MIT", |
||||||
|
"scripts": { |
||||||
|
"prebuild": "rimraf dist", |
||||||
|
"build": "nest build", |
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", |
||||||
|
"start": "nest start", |
||||||
|
"start:dev": "nest start --watch", |
||||||
|
"start:debug": "nest start --debug --watch", |
||||||
|
"start:prod": "node dist/main", |
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", |
||||||
|
"test": "jest", |
||||||
|
"test:watch": "jest --watch", |
||||||
|
"test:cov": "jest --coverage", |
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", |
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@nestjs/common": "^10.0.0", |
||||||
|
"@nestjs/core": "^10.0.0", |
||||||
|
"@nestjs/platform-express": "^10.0.0", |
||||||
|
"reflect-metadata": "^0.1.13", |
||||||
|
"rxjs": "^7.8.1" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@nestjs/cli": "^10.0.0", |
||||||
|
"@nestjs/schematics": "^10.0.0", |
||||||
|
"@nestjs/testing": "^10.0.0", |
||||||
|
"@types/express": "^4.17.17", |
||||||
|
"@types/jest": "^29.5.2", |
||||||
|
"@types/node": "^20.3.1", |
||||||
|
"@types/supertest": "^6.0.0", |
||||||
|
"@typescript-eslint/eslint-plugin": "^6.0.0", |
||||||
|
"@typescript-eslint/parser": "^6.0.0", |
||||||
|
"eslint": "^8.42.0", |
||||||
|
"eslint-config-prettier": "^9.0.0", |
||||||
|
"eslint-plugin-prettier": "^5.0.0", |
||||||
|
"jest": "^29.5.0", |
||||||
|
"prettier": "^3.0.0", |
||||||
|
"rimraf": "^5.0.1", |
||||||
|
"source-map-support": "^0.5.21", |
||||||
|
"supertest": "^6.3.3", |
||||||
|
"ts-jest": "^29.1.0", |
||||||
|
"ts-loader": "^9.4.3", |
||||||
|
"ts-node": "^10.9.1", |
||||||
|
"tsconfig-paths": "^4.2.0", |
||||||
|
"typescript": "^5.1.3" |
||||||
|
}, |
||||||
|
"jest": { |
||||||
|
"moduleFileExtensions": [ |
||||||
|
"js", |
||||||
|
"json", |
||||||
|
"ts" |
||||||
|
], |
||||||
|
"rootDir": "src", |
||||||
|
"testRegex": ".*\\.spec\\.ts$", |
||||||
|
"transform": { |
||||||
|
"^.+\\.(t|j)s$": "ts-jest" |
||||||
|
}, |
||||||
|
"collectCoverageFrom": [ |
||||||
|
"**/*.(t|j)s" |
||||||
|
], |
||||||
|
"coverageDirectory": "../coverage", |
||||||
|
"testEnvironment": "node" |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import { Controller, Get } from '@nestjs/common'; |
||||||
|
import { AppService } from './app.service'; |
||||||
|
|
||||||
|
@Controller() |
||||||
|
export class AppController { |
||||||
|
constructor(private readonly appService: AppService) {} |
||||||
|
|
||||||
|
@Get() |
||||||
|
getHello(): string { |
||||||
|
return this.appService.getHello(); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,10 @@ |
|||||||
|
import { Module } from '@nestjs/common'; |
||||||
|
import { AppController } from './app.controller'; |
||||||
|
import { AppService } from './app.service'; |
||||||
|
|
||||||
|
@Module({ |
||||||
|
imports: [], |
||||||
|
controllers: [AppController], |
||||||
|
providers: [AppService], |
||||||
|
}) |
||||||
|
export class AppModule {} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import { Injectable } from '@nestjs/common'; |
||||||
|
|
||||||
|
@Injectable() |
||||||
|
export class AppService { |
||||||
|
getHello(): string { |
||||||
|
return 'Hello World!'; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,8 @@ |
|||||||
|
import { NestFactory } from '@nestjs/core'; |
||||||
|
import { AppModule } from './app.module'; |
||||||
|
|
||||||
|
async function bootstrap() { |
||||||
|
const app = await NestFactory.create(AppModule); |
||||||
|
await app.listen(3000); |
||||||
|
} |
||||||
|
bootstrap(); |
||||||
@ -0,0 +1,24 @@ |
|||||||
|
{ |
||||||
|
"compilerOptions": { |
||||||
|
"module": "commonjs", |
||||||
|
"declaration": true, |
||||||
|
"removeComments": true, |
||||||
|
"emitDecoratorMetadata": true, |
||||||
|
"experimentalDecorators": true, |
||||||
|
"allowSyntheticDefaultImports": true, |
||||||
|
"target": "ES2021", |
||||||
|
"sourceMap": true, |
||||||
|
"outDir": "./dist", |
||||||
|
"baseUrl": "./", |
||||||
|
"incremental": true, |
||||||
|
"skipLibCheck": true, |
||||||
|
"strictNullChecks": false, |
||||||
|
"noImplicitAny": false, |
||||||
|
"strictBindCallApply": false, |
||||||
|
"forceConsistentCasingInFileNames": false, |
||||||
|
"noFallthroughCasesInSwitch": false, |
||||||
|
"paths": { |
||||||
|
"@/*": ["src/*"] |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace AutoStore\Application\Services; |
||||||
|
|
||||||
|
use AutoStore\Application\Interfaces\IUserRepository; |
||||||
|
use AutoStore\Domain\Entities\User; |
||||||
|
use Psr\Log\LoggerInterface; |
||||||
|
|
||||||
|
class UserInitializationService |
||||||
|
{ |
||||||
|
private IUserRepository $userRepository; |
||||||
|
private LoggerInterface $logger; |
||||||
|
|
||||||
|
public function __construct(IUserRepository $userRepository, LoggerInterface $logger) |
||||||
|
{ |
||||||
|
$this->userRepository = $userRepository; |
||||||
|
$this->logger = $logger; |
||||||
|
} |
||||||
|
|
||||||
|
public function createDefaultUsers(): void |
||||||
|
{ |
||||||
|
$defaultUsers = [ |
||||||
|
['username' => 'admin', 'password' => 'admin'], |
||||||
|
['username' => 'user', 'password' => 'user'] |
||||||
|
]; |
||||||
|
|
||||||
|
foreach ($defaultUsers as $userData) { |
||||||
|
$user = new User( |
||||||
|
uniqid('user_', true), |
||||||
|
$userData['username'], |
||||||
|
password_hash($userData['password'], PASSWORD_DEFAULT) |
||||||
|
); |
||||||
|
$this->userRepository->save($user); |
||||||
|
} |
||||||
|
|
||||||
|
$this->logger->info("Created default users"); |
||||||
|
} |
||||||
|
} |
||||||
@ -1,19 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Filters; |
|
||||||
|
|
||||||
class FilterCondition extends FilterExpr |
|
||||||
{ |
|
||||||
public string $field; |
|
||||||
public string $operator; |
|
||||||
public $value; |
|
||||||
|
|
||||||
public function __construct(string $field, string $operator, $value = null) |
|
||||||
{ |
|
||||||
$this->field = $field; |
|
||||||
$this->operator = $operator; |
|
||||||
$this->value = $value; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,9 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Filters; |
|
||||||
|
|
||||||
abstract class FilterExpr |
|
||||||
{ |
|
||||||
} |
|
||||||
@ -1,22 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Filters; |
|
||||||
|
|
||||||
class FilterGroup extends FilterExpr |
|
||||||
{ |
|
||||||
public string $type; |
|
||||||
public array $children = []; |
|
||||||
|
|
||||||
public function __construct(string $type = 'AND') |
|
||||||
{ |
|
||||||
$this->type = strtoupper($type); |
|
||||||
} |
|
||||||
|
|
||||||
public function add(FilterExpr $expr): self |
|
||||||
{ |
|
||||||
$this->children[] = $expr; |
|
||||||
return $this; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,20 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Filters; |
|
||||||
|
|
||||||
class FilterOperator |
|
||||||
{ |
|
||||||
public const EQUALS = 'equals'; |
|
||||||
public const NOT_EQUALS = 'not_equals'; |
|
||||||
public const GREATER_THAN = 'greater_than'; |
|
||||||
public const LESS_THAN = 'less_than'; |
|
||||||
public const GREATER_EQ = 'greater_eq'; |
|
||||||
public const LESS_EQ = 'less_eq'; |
|
||||||
public const IN = 'in'; |
|
||||||
public const NOT_IN = 'not_in'; |
|
||||||
public const LIKE = 'like'; |
|
||||||
public const IS_NULL = 'is_null'; |
|
||||||
public const IS_NOT_NULL = 'is_not_null'; |
|
||||||
} |
|
||||||
@ -1,236 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Filters; |
|
||||||
|
|
||||||
class FilterSpecification |
|
||||||
{ |
|
||||||
private FilterGroup $rootGroup; |
|
||||||
private FilterGroup $currentGroup; |
|
||||||
private array $groupStack = []; |
|
||||||
private ?string $lastField = null; |
|
||||||
|
|
||||||
public function __construct(?string $field = null) |
|
||||||
{ |
|
||||||
$this->rootGroup = new FilterGroup('AND'); |
|
||||||
$this->currentGroup = $this->rootGroup; |
|
||||||
if ($field !== null) { |
|
||||||
$this->field($field); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
public function field(string $field): self |
|
||||||
{ |
|
||||||
$this->lastField = $field; |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function equals($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::EQUALS, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function notEquals($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::NOT_EQUALS, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function greaterThan($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::GREATER_THAN, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function lessThan($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LESS_THAN, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function greaterEq($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::GREATER_EQ, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function lessEq($value): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LESS_EQ, $value)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function in(array $values): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IN, $values)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function notIn(array $values): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::NOT_IN, $values)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function like(string $pattern): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, $pattern)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function contains(string $needle): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, "%$needle%")); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function startsWith(string $prefix): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, $prefix . '%')); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function endsWith(string $suffix): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::LIKE, '%' . $suffix)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function isNull(): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IS_NULL)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function isNotNull(): self |
|
||||||
{ |
|
||||||
$this->currentGroup->add(new FilterCondition($this->consumeField(), FilterOperator::IS_NOT_NULL)); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function or(?string $field = null): self |
|
||||||
{ |
|
||||||
$newGroup = new FilterGroup('OR'); |
|
||||||
if (!empty($this->currentGroup->children)) { |
|
||||||
$lastExpr = array_pop($this->currentGroup->children); |
|
||||||
$newGroup->add($lastExpr); |
|
||||||
} |
|
||||||
$this->currentGroup->add($newGroup); |
|
||||||
$this->groupStack[] = $this->currentGroup; |
|
||||||
$this->currentGroup = $newGroup; |
|
||||||
if ($field !== null) { |
|
||||||
$this->field($field); |
|
||||||
} |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function and(?string $field = null): self |
|
||||||
{ |
|
||||||
$newGroup = new FilterGroup('AND'); |
|
||||||
if (!empty($this->currentGroup->children)) { |
|
||||||
$lastExpr = array_pop($this->currentGroup->children); |
|
||||||
$newGroup->add($lastExpr); |
|
||||||
} |
|
||||||
$this->currentGroup->add($newGroup); |
|
||||||
$this->groupStack[] = $this->currentGroup; |
|
||||||
$this->currentGroup = $newGroup; |
|
||||||
if ($field !== null) { |
|
||||||
$this->field($field); |
|
||||||
} |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function close(): self |
|
||||||
{ |
|
||||||
if (empty($this->groupStack)) { |
|
||||||
throw new \LogicException('No group to close.'); |
|
||||||
} |
|
||||||
$this->currentGroup = array_pop($this->groupStack); |
|
||||||
return $this; |
|
||||||
} |
|
||||||
|
|
||||||
public function getExpression(): FilterExpr |
|
||||||
{ |
|
||||||
return $this->rootGroup; |
|
||||||
} |
|
||||||
|
|
||||||
public function matches(array $data): bool |
|
||||||
{ |
|
||||||
return $this->evaluateExpression($this->rootGroup, $data); |
|
||||||
} |
|
||||||
|
|
||||||
private function evaluateExpression(FilterExpr $expr, array $data): bool |
|
||||||
{ |
|
||||||
if ($expr instanceof FilterCondition) { |
|
||||||
return $this->evaluateCondition($expr, $data); |
|
||||||
} |
|
||||||
|
|
||||||
if ($expr instanceof FilterGroup) { |
|
||||||
return $this->evaluateGroup($expr, $data); |
|
||||||
} |
|
||||||
|
|
||||||
throw new \LogicException('Unknown filter expression type.'); |
|
||||||
} |
|
||||||
|
|
||||||
private function evaluateCondition(FilterCondition $condition, array $data): bool |
|
||||||
{ |
|
||||||
if (!array_key_exists($condition->field, $data)) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
|
|
||||||
$value = $data[$condition->field]; |
|
||||||
|
|
||||||
switch ($condition->operator) { |
|
||||||
case FilterOperator::EQUALS: |
|
||||||
return $value == $condition->value; |
|
||||||
case FilterOperator::NOT_EQUALS: |
|
||||||
return $value != $condition->value; |
|
||||||
case FilterOperator::GREATER_THAN: |
|
||||||
return $value > $condition->value; |
|
||||||
case FilterOperator::LESS_THAN: |
|
||||||
return $value < $condition->value; |
|
||||||
case FilterOperator::GREATER_EQ: |
|
||||||
return $value >= $condition->value; |
|
||||||
case FilterOperator::LESS_EQ: |
|
||||||
return $value <= $condition->value; |
|
||||||
case FilterOperator::IN: |
|
||||||
return in_array($value, $condition->value); |
|
||||||
case FilterOperator::NOT_IN: |
|
||||||
return !in_array($value, $condition->value); |
|
||||||
case FilterOperator::LIKE: |
|
||||||
return str_contains((string)$value, str_replace('%', '', $condition->value)); |
|
||||||
case FilterOperator::IS_NULL: |
|
||||||
return $value === null; |
|
||||||
case FilterOperator::IS_NOT_NULL: |
|
||||||
return $value !== null; |
|
||||||
default: |
|
||||||
throw new \LogicException("Unknown operator: {$condition->operator}"); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private function evaluateGroup(FilterGroup $group, array $data): bool |
|
||||||
{ |
|
||||||
if (empty($group->children)) { |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
$results = []; |
|
||||||
foreach ($group->children as $child) { |
|
||||||
$results[] = $this->evaluateExpression($child, $data); |
|
||||||
} |
|
||||||
|
|
||||||
return $group->type === 'AND' ? !in_array(false, $results) : in_array(true, $results); |
|
||||||
} |
|
||||||
|
|
||||||
private function consumeField(): string |
|
||||||
{ |
|
||||||
if ($this->lastField === null) { |
|
||||||
throw new \LogicException("No field specified."); |
|
||||||
} |
|
||||||
$field = $this->lastField; |
|
||||||
$this->lastField = null; |
|
||||||
return $field; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -1,25 +0,0 @@ |
|||||||
<?php |
|
||||||
|
|
||||||
declare(strict_types=1); |
|
||||||
|
|
||||||
namespace AutoStore\Domain\Policies; |
|
||||||
|
|
||||||
use AutoStore\Domain\Entities\Item; |
|
||||||
use AutoStore\Domain\Filters\FilterSpecification; |
|
||||||
use DateTimeImmutable; |
|
||||||
|
|
||||||
class ItemExpirationPolicy |
|
||||||
{ |
|
||||||
public function isExpired(Item $item, DateTimeImmutable $currentTime): bool |
|
||||||
{ |
|
||||||
return $item->getExpirationDate() <= $currentTime; |
|
||||||
} |
|
||||||
|
|
||||||
public function getExpirationSpec(DateTimeImmutable $currentTime): FilterSpecification |
|
||||||
{ |
|
||||||
$specification = new FilterSpecification('expirationDate'); |
|
||||||
$specification->lessEq($currentTime->format('Y-m-d H:i:s')); |
|
||||||
|
|
||||||
return $specification; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -0,0 +1,24 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace AutoStore\Domain\Specifications; |
||||||
|
|
||||||
|
use AutoStore\Domain\Entities\Item; |
||||||
|
use DateTimeImmutable; |
||||||
|
|
||||||
|
class ItemExpirationSpec |
||||||
|
{ |
||||||
|
public function isExpired(Item $item, DateTimeImmutable $currentTime): bool |
||||||
|
{ |
||||||
|
// return $item->getExpirationDate() <= $currentTime; |
||||||
|
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')) |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,266 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
namespace AutoStore\Domain\Specifications; |
||||||
|
|
||||||
|
class Specification |
||||||
|
{ |
||||||
|
private array $spec; |
||||||
|
|
||||||
|
public function __construct(array $spec) |
||||||
|
{ |
||||||
|
$this->spec = $spec; |
||||||
|
} |
||||||
|
|
||||||
|
public function getSpec(): array |
||||||
|
{ |
||||||
|
return $this->spec; |
||||||
|
} |
||||||
|
|
||||||
|
public function match($object): bool |
||||||
|
{ |
||||||
|
return $this->evaluateSpec($this->spec, $object); |
||||||
|
} |
||||||
|
|
||||||
|
private function evaluateSpec(array $spec, $object): bool |
||||||
|
{ |
||||||
|
// Handle logical groups |
||||||
|
if (isset($spec[Spec::GROUP_AND])) { |
||||||
|
foreach ($spec[Spec::GROUP_AND] as $subSpec) { |
||||||
|
if (!$this->evaluateSpec($subSpec, $object)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
if (isset($spec[Spec::GROUP_OR])) { |
||||||
|
foreach ($spec[Spec::GROUP_OR] as $subSpec) { |
||||||
|
if ($this->evaluateSpec($subSpec, $object)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
if (isset($spec[Spec::GROUP_NOT])) { |
||||||
|
return !$this->evaluateSpec($spec[Spec::GROUP_NOT], $object); |
||||||
|
} |
||||||
|
|
||||||
|
// Handle simple conditions [field, op, value] |
||||||
|
[$field, $op, $value] = $spec; |
||||||
|
|
||||||
|
// Check if field exists in the object |
||||||
|
$getterMethod = 'get' . ucfirst($field); |
||||||
|
if (method_exists($object, $getterMethod)) { |
||||||
|
$fieldValue = $object->$getterMethod(); |
||||||
|
} elseif (property_exists($object, $field)) { |
||||||
|
$fieldValue = $object->$field; |
||||||
|
} elseif (isset($object->$field)) { |
||||||
|
$fieldValue = $object->$field; |
||||||
|
} else { |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
// Handle DateTimeImmutable comparison |
||||||
|
if ($fieldValue instanceof \DateTimeInterface && is_string($value)) { |
||||||
|
try { |
||||||
|
$valueDate = new \DateTimeImmutable($value); |
||||||
|
return $this->compareDateTime($fieldValue, $op, $valueDate); |
||||||
|
} catch (\Exception $e) { |
||||||
|
// If value is not a valid date string, fall back to regular comparison |
||||||
|
} |
||||||
|
} elseif (is_string($fieldValue) && $value instanceof \DateTimeInterface) { |
||||||
|
try { |
||||||
|
$fieldValueDate = new \DateTimeImmutable($fieldValue); |
||||||
|
return $this->compareDateTime($fieldValueDate, $op, $value); |
||||||
|
} catch (\Exception $e) { |
||||||
|
// If fieldValue is not a valid date string, fall back to regular comparison |
||||||
|
} |
||||||
|
} elseif ($fieldValue instanceof \DateTimeInterface && $value instanceof \DateTimeInterface) { |
||||||
|
return $this->compareDateTime($fieldValue, $op, $value); |
||||||
|
} |
||||||
|
|
||||||
|
// Evaluate based on operator |
||||||
|
switch ($op) { |
||||||
|
case Spec::OP_EQ: |
||||||
|
return $fieldValue == $value; |
||||||
|
case Spec::OP_NEQ: |
||||||
|
return $fieldValue != $value; |
||||||
|
case Spec::OP_GT: |
||||||
|
return $fieldValue > $value; |
||||||
|
case Spec::OP_GTE: |
||||||
|
return $fieldValue >= $value; |
||||||
|
case Spec::OP_LT: |
||||||
|
return $fieldValue < $value; |
||||||
|
case Spec::OP_LTE: |
||||||
|
return $fieldValue <= $value; |
||||||
|
case Spec::OP_IN: |
||||||
|
return is_array($value) && in_array($fieldValue, $value); |
||||||
|
case Spec::OP_NIN: |
||||||
|
return is_array($value) && !in_array($fieldValue, $value); |
||||||
|
default: |
||||||
|
return false; // Unknown operator |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function compareDateTime(\DateTimeInterface $fieldValue, string $op, \DateTimeInterface $value): bool |
||||||
|
{ |
||||||
|
switch ($op) { |
||||||
|
case Spec::OP_EQ: |
||||||
|
return $fieldValue->format('Y-m-d H:i:s') === $value->format('Y-m-d H:i:s'); |
||||||
|
case Spec::OP_NEQ: |
||||||
|
return $fieldValue->format('Y-m-d H:i:s') !== $value->format('Y-m-d H:i:s'); |
||||||
|
case Spec::OP_GT: |
||||||
|
return $fieldValue > $value; |
||||||
|
case Spec::OP_GTE: |
||||||
|
return $fieldValue >= $value; |
||||||
|
case Spec::OP_LT: |
||||||
|
return $fieldValue < $value; |
||||||
|
case Spec::OP_LTE: |
||||||
|
return $fieldValue <= $value; |
||||||
|
default: |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Spec |
||||||
|
{ |
||||||
|
// Logical group operators |
||||||
|
public const GROUP_AND = 'AND'; |
||||||
|
public const GROUP_OR = 'OR'; |
||||||
|
public const GROUP_NOT = 'NOT'; |
||||||
|
|
||||||
|
// Comparison operators |
||||||
|
public const OP_EQ = '='; |
||||||
|
public const OP_NEQ = '!='; |
||||||
|
public const OP_GT = '>'; |
||||||
|
public const OP_GTE = '>='; |
||||||
|
public const OP_LT = '<'; |
||||||
|
public const OP_LTE = '<='; |
||||||
|
public const OP_IN = 'IN'; |
||||||
|
public const OP_NIN = 'NOT IN'; |
||||||
|
|
||||||
|
// Logical group helpers |
||||||
|
public static function and(array $conditions): array { |
||||||
|
return [self::GROUP_AND => $conditions]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function or(array $conditions): array { |
||||||
|
return [self::GROUP_OR => $conditions]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function not(array $condition): array { |
||||||
|
return [self::GROUP_NOT => $condition]; |
||||||
|
} |
||||||
|
|
||||||
|
// Condition helpers |
||||||
|
public static function eq(string $field, $value): array { |
||||||
|
return [$field, self::OP_EQ, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function neq(string $field, $value): array { |
||||||
|
return [$field, self::OP_NEQ, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function gt(string $field, $value): array { |
||||||
|
return [$field, self::OP_GT, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function gte(string $field, $value): array { |
||||||
|
return [$field, self::OP_GTE, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function lt(string $field, $value): array { |
||||||
|
return [$field, self::OP_LT, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function lte(string $field, $value): array { |
||||||
|
return [$field, self::OP_LTE, $value]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function in(string $field, array $values): array { |
||||||
|
return [$field, self::OP_IN, $values]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function nin(string $field, array $values): array { |
||||||
|
return [$field, self::OP_NIN, $values]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/* |
||||||
|
|
||||||
|
// ----------------- |
||||||
|
// 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)) |
||||||
|
]); |
||||||
|
|
||||||
|
$params = []; |
||||||
|
$sqlRenderer = new SqlRenderer(); |
||||||
|
$whereClause = $sqlRenderer->render($spec, $params); |
||||||
|
|
||||||
|
echo "SQL: " . $whereClause . "\n"; |
||||||
|
echo "Params: " . json_encode($params, JSON_PRETTY_PRINT) . "\n"; |
||||||
|
|
||||||
|
// ----------------- |
||||||
|
// Example SQL renderer implementation in infrastructure layer |
||||||
|
|
||||||
|
class SqlRenderer |
||||||
|
{ |
||||||
|
private $operatorMap = [ |
||||||
|
Spec::OP_EQ => '=', |
||||||
|
Spec::OP_NEQ => '<>', |
||||||
|
Spec::OP_GT => '>', |
||||||
|
Spec::OP_GTE => '>=', |
||||||
|
Spec::OP_LT => '<', |
||||||
|
Spec::OP_LTE => '<=', |
||||||
|
Spec::OP_IN => 'IN', |
||||||
|
Spec::OP_NIN => 'NOT IN', |
||||||
|
]; |
||||||
|
|
||||||
|
public function render($spec, array &$params = []): string |
||||||
|
{ |
||||||
|
// Logical groups |
||||||
|
if (isset($spec[Spec::GROUP_AND])) { |
||||||
|
$parts = array_map(fn($s) => $this->render($s, $params), $spec[Spec::GROUP_AND]); |
||||||
|
return '(' . implode(' AND ', $parts) . ')'; |
||||||
|
} |
||||||
|
if (isset($spec[Spec::GROUP_OR])) { |
||||||
|
$parts = array_map(fn($s) => $this->render($s, $params), $spec[Spec::GROUP_OR]); |
||||||
|
return '(' . implode(' OR ', $parts) . ')'; |
||||||
|
} |
||||||
|
if (isset($spec[Spec::GROUP_NOT])) { |
||||||
|
return 'NOT (' . $this->render($spec[Spec::GROUP_NOT], $params) . ')'; |
||||||
|
} |
||||||
|
|
||||||
|
// It's a simple [field, op, value] |
||||||
|
[$field, $op, $value] = $spec; |
||||||
|
$renderOp = $this->operatorMap[$op] ?? $op; |
||||||
|
|
||||||
|
if ($op === Spec::OP_IN || $op === Spec::OP_NIN) { |
||||||
|
if (!$value) { |
||||||
|
// Empty IN list: render a false condition |
||||||
|
return ($op === Spec::OP_IN) ? '0=1' : '1=1'; |
||||||
|
} |
||||||
|
$placeholders = array_fill(0, count($value), '?'); |
||||||
|
foreach ($value as $v) { |
||||||
|
$params[] = $v; |
||||||
|
} |
||||||
|
return sprintf("%s %s (%s)", $field, $renderOp, implode(',', $placeholders)); |
||||||
|
} |
||||||
|
|
||||||
|
$params[] = $value; |
||||||
|
return sprintf("%s %s ?", $field, $renderOp); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
*/ |
||||||
|
|
||||||
@ -0,0 +1,39 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace AutoStore\Infrastructure\Mappers; |
||||||
|
|
||||||
|
use AutoStore\Domain\Entities\Item; |
||||||
|
use AutoStore\Domain\Exceptions\DomainException; |
||||||
|
use DateTimeImmutable; |
||||||
|
|
||||||
|
class ItemMapper |
||||||
|
{ |
||||||
|
public static function toArray(Item $item): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
'id' => $item->getId(), |
||||||
|
'name' => $item->getName(), |
||||||
|
'expirationDate' => $item->getExpirationDate()->format('Y-m-d H:i:s'), |
||||||
|
'orderUrl' => $item->getOrderUrl(), |
||||||
|
'userId' => $item->getUserId(), |
||||||
|
'createdAt' => $item->getCreatedAt()->format('Y-m-d H:i:s') |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function fromArray(array $data): Item |
||||||
|
{ |
||||||
|
if (!isset($data['id'], $data['name'], $data['expirationDate'], $data['orderUrl'], $data['userId'])) { |
||||||
|
throw new DomainException('Invalid item data'); |
||||||
|
} |
||||||
|
|
||||||
|
return new Item( |
||||||
|
$data['id'], |
||||||
|
$data['name'], |
||||||
|
new DateTimeImmutable($data['expirationDate']), |
||||||
|
$data['orderUrl'], |
||||||
|
$data['userId'] |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace AutoStore\Infrastructure\Mappers; |
||||||
|
|
||||||
|
use AutoStore\Domain\Entities\User; |
||||||
|
use AutoStore\Domain\Exceptions\DomainException; |
||||||
|
use DateTimeImmutable; |
||||||
|
|
||||||
|
class UserMapper |
||||||
|
{ |
||||||
|
public static function toArray(User $user): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
'id' => $user->getId(), |
||||||
|
'username' => $user->getUsername(), |
||||||
|
'passwordHash' => $user->getPasswordHash(), |
||||||
|
'createdAt' => $user->getCreatedAt()->format('Y-m-d\TH:i:s.uP'), |
||||||
|
'updatedAt' => $user->getUpdatedAt()?->format('Y-m-d\TH:i:s.uP'), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
public static function fromArray(array $data): User |
||||||
|
{ |
||||||
|
if (!isset($data['id'], $data['username'], $data['passwordHash'])) { |
||||||
|
throw new DomainException('Invalid user data'); |
||||||
|
} |
||||||
|
|
||||||
|
return new User( |
||||||
|
$data['id'], |
||||||
|
$data['username'], |
||||||
|
$data['passwordHash'] |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,688 @@ |
|||||||
|
<?php |
||||||
|
|
||||||
|
declare(strict_types=1); |
||||||
|
|
||||||
|
namespace AutoStore\Tests\Unit\Domain\Specifications; |
||||||
|
|
||||||
|
use AutoStore\Domain\Specifications\Specification; |
||||||
|
use AutoStore\Domain\Specifications\Spec; |
||||||
|
use DateTimeImmutable; |
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use stdClass; |
||||||
|
|
||||||
|
class SpecificationTest extends TestCase |
||||||
|
{ |
||||||
|
// Test data constants |
||||||
|
private const TEST_STRING = 'test-value'; |
||||||
|
private const TEST_INT = 42; |
||||||
|
private const TEST_FLOAT = 3.14; |
||||||
|
private const TEST_DATE = '2023-01-15 10:30:00'; |
||||||
|
private const TEST_DATE_2 = '2023-02-15 10:30:00'; |
||||||
|
|
||||||
|
private function createSimpleObject(array $data): stdClass |
||||||
|
{ |
||||||
|
$object = new stdClass(); |
||||||
|
foreach ($data as $key => $value) { |
||||||
|
$object->$key = $value; |
||||||
|
} |
||||||
|
return $object; |
||||||
|
} |
||||||
|
|
||||||
|
private function createComplexObject(array $data): TestObject |
||||||
|
{ |
||||||
|
return new TestObject($data); |
||||||
|
} |
||||||
|
|
||||||
|
// EQ Operator Tests |
||||||
|
|
||||||
|
public function testWhenUsingEqWithStringThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('name', self::TEST_STRING)); |
||||||
|
$matchingObject = $this->createSimpleObject(['name' => self::TEST_STRING]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['name' => 'different']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingEqWithIntegerThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('age', self::TEST_INT)); |
||||||
|
$matchingObject = $this->createSimpleObject(['age' => self::TEST_INT]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['age' => 100]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingEqWithFloatThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('price', self::TEST_FLOAT)); |
||||||
|
$matchingObject = $this->createSimpleObject(['price' => self::TEST_FLOAT]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['price' => 1.0]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingEqWithComplexObjectGetterThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('name', self::TEST_STRING)); |
||||||
|
$matchingObject = $this->createComplexObject(['name' => self::TEST_STRING]); |
||||||
|
$nonMatchingObject = $this->createComplexObject(['name' => 'different']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
// NEQ Operator Tests |
||||||
|
|
||||||
|
public function testWhenUsingNeqThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::neq('status', 'inactive')); |
||||||
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'inactive']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
// Comparison Operators Tests |
||||||
|
|
||||||
|
public function testWhenUsingGtThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::gt('score', 80)); |
||||||
|
$matchingObject = $this->createSimpleObject(['score' => 90]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['score' => 70]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingGteThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::gte('score', 80)); |
||||||
|
$matchingObject1 = $this->createSimpleObject(['score' => 80]); |
||||||
|
$matchingObject2 = $this->createSimpleObject(['score' => 90]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['score' => 70]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult1 = $spec->match($matchingObject1); |
||||||
|
$matchResult2 = $spec->match($matchingObject2); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult1); |
||||||
|
$this->assertTrue($matchResult2); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingLtThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::lt('score', 80)); |
||||||
|
$matchingObject = $this->createSimpleObject(['score' => 70]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['score' => 90]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingLteThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::lte('score', 80)); |
||||||
|
$matchingObject1 = $this->createSimpleObject(['score' => 80]); |
||||||
|
$matchingObject2 = $this->createSimpleObject(['score' => 70]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['score' => 90]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult1 = $spec->match($matchingObject1); |
||||||
|
$matchResult2 = $spec->match($matchingObject2); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult1); |
||||||
|
$this->assertTrue($matchResult2); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
// IN Operator Tests |
||||||
|
|
||||||
|
public function testWhenUsingInThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$validValues = ['admin', 'moderator', 'editor']; |
||||||
|
$spec = new Specification(Spec::in('role', $validValues)); |
||||||
|
$matchingObject = $this->createSimpleObject(['role' => 'admin']); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['role' => 'user']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingInWithEmptyArrayThenNeverMatches(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::in('role', [])); |
||||||
|
$testObject = $this->createSimpleObject(['role' => 'admin']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertFalse($result); |
||||||
|
} |
||||||
|
|
||||||
|
// NOT IN Operator Tests |
||||||
|
|
||||||
|
public function testWhenUsingNotInThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$invalidValues = ['banned', 'suspended']; |
||||||
|
$spec = new Specification(Spec::nin('status', $invalidValues)); |
||||||
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
// DateTime Tests |
||||||
|
|
||||||
|
public function testWhenUsingDateTimeComparisonWithStringsThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::lt('createdAt', self::TEST_DATE_2)); |
||||||
|
$matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE_2]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingDateTimeComparisonWithDateTimeObjectsThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$testDate = new DateTimeImmutable(self::TEST_DATE); |
||||||
|
$spec = new Specification(Spec::lte('expirationDate', $testDate)); |
||||||
|
$matchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE)]); |
||||||
|
$nonMatchingObject = $this->createComplexObject(['expirationDate' => new DateTimeImmutable(self::TEST_DATE_2)]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingDateTimeComparisonWithMixedTypesThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$testDate = new DateTimeImmutable(self::TEST_DATE); |
||||||
|
$spec = new Specification(Spec::eq('createdAt', $testDate)); |
||||||
|
$matchingObject = $this->createSimpleObject(['createdAt' => self::TEST_DATE]); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($matchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($result); |
||||||
|
} |
||||||
|
|
||||||
|
// AND Group Tests |
||||||
|
|
||||||
|
public function testWhenUsingAndGroupThenMatchesOnlyWhenAllConditionsMet(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::and([ |
||||||
|
Spec::eq('status', 'active'), |
||||||
|
Spec::gte('score', 80), |
||||||
|
Spec::in('role', ['admin', 'moderator']) |
||||||
|
])); |
||||||
|
$matchingObject = $this->createSimpleObject([ |
||||||
|
'status' => 'active', |
||||||
|
'score' => 85, |
||||||
|
'role' => 'admin' |
||||||
|
]); |
||||||
|
$nonMatchingObject1 = $this->createSimpleObject([ |
||||||
|
'status' => 'inactive', |
||||||
|
'score' => 85, |
||||||
|
'role' => 'admin' |
||||||
|
]); |
||||||
|
$nonMatchingObject2 = $this->createSimpleObject([ |
||||||
|
'status' => 'active', |
||||||
|
'score' => 70, |
||||||
|
'role' => 'admin' |
||||||
|
]); |
||||||
|
$nonMatchingObject3 = $this->createSimpleObject([ |
||||||
|
'status' => 'active', |
||||||
|
'score' => 85, |
||||||
|
'role' => 'user' |
||||||
|
]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult1 = $spec->match($nonMatchingObject1); |
||||||
|
$noMatchResult2 = $spec->match($nonMatchingObject2); |
||||||
|
$noMatchResult3 = $spec->match($nonMatchingObject3); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult1); |
||||||
|
$this->assertFalse($noMatchResult2); |
||||||
|
$this->assertFalse($noMatchResult3); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingEmptyAndGroupThenAlwaysMatches(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::and([])); |
||||||
|
$testObject = $this->createSimpleObject(['any' => 'value']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($result); |
||||||
|
} |
||||||
|
|
||||||
|
// OR Group Tests |
||||||
|
|
||||||
|
public function testWhenUsingOrGroupThenMatchesWhenAnyConditionMet(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::or([ |
||||||
|
Spec::eq('role', 'admin'), |
||||||
|
Spec::gte('score', 90), |
||||||
|
Spec::in('department', ['IT', 'HR']) |
||||||
|
])); |
||||||
|
$matchingObject1 = $this->createSimpleObject([ |
||||||
|
'role' => 'admin', |
||||||
|
'score' => 70, |
||||||
|
'department' => 'Finance' |
||||||
|
]); |
||||||
|
$matchingObject2 = $this->createSimpleObject([ |
||||||
|
'role' => 'user', |
||||||
|
'score' => 95, |
||||||
|
'department' => 'Finance' |
||||||
|
]); |
||||||
|
$matchingObject3 = $this->createSimpleObject([ |
||||||
|
'role' => 'user', |
||||||
|
'score' => 70, |
||||||
|
'department' => 'IT' |
||||||
|
]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject([ |
||||||
|
'role' => 'user', |
||||||
|
'score' => 70, |
||||||
|
'department' => 'Finance' |
||||||
|
]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult1 = $spec->match($matchingObject1); |
||||||
|
$matchResult2 = $spec->match($matchingObject2); |
||||||
|
$matchResult3 = $spec->match($matchingObject3); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult1); |
||||||
|
$this->assertTrue($matchResult2); |
||||||
|
$this->assertTrue($matchResult3); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingEmptyOrGroupThenNeverMatches(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::or([])); |
||||||
|
$testObject = $this->createSimpleObject(['any' => 'value']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertFalse($result); |
||||||
|
} |
||||||
|
|
||||||
|
// NOT Group Tests |
||||||
|
|
||||||
|
public function testWhenUsingNotGroupThenInvertsCondition(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::not(Spec::eq('status', 'banned'))); |
||||||
|
$matchingObject = $this->createSimpleObject(['status' => 'active']); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['status' => 'banned']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
// Complex Nested Groups Tests |
||||||
|
|
||||||
|
public function testWhenUsingNestedAndOrGroupsThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::and([ |
||||||
|
Spec::eq('status', 'active'), |
||||||
|
Spec::or([ |
||||||
|
Spec::gte('score', 80), |
||||||
|
Spec::in('role', ['admin', 'moderator']) |
||||||
|
]) |
||||||
|
])); |
||||||
|
$matchingObject1 = $this->createSimpleObject([ |
||||||
|
'status' => 'active', |
||||||
|
'score' => 85, |
||||||
|
'role' => 'user' |
||||||
|
]); |
||||||
|
$matchingObject2 = $this->createSimpleObject([ |
||||||
|
'status' => 'active', |
||||||
|
'score' => 70, |
||||||
|
'role' => 'admin' |
||||||
|
]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject([ |
||||||
|
'status' => 'inactive', |
||||||
|
'score' => 85, |
||||||
|
'role' => 'user' |
||||||
|
]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult1 = $spec->match($matchingObject1); |
||||||
|
$matchResult2 = $spec->match($matchingObject2); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult1); |
||||||
|
$this->assertTrue($matchResult2); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingTripleNestedGroupsThenMatchesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::and([ |
||||||
|
Spec::eq('active', true), |
||||||
|
Spec::not(Spec::or([ |
||||||
|
Spec::eq('role', 'banned'), |
||||||
|
Spec::eq('status', 'suspended') |
||||||
|
])) |
||||||
|
])); |
||||||
|
$matchingObject = $this->createSimpleObject([ |
||||||
|
'active' => true, |
||||||
|
'role' => 'user', |
||||||
|
'status' => 'active' |
||||||
|
]); |
||||||
|
$nonMatchingObject1 = $this->createSimpleObject([ |
||||||
|
'active' => false, |
||||||
|
'role' => 'user', |
||||||
|
'status' => 'active' |
||||||
|
]); |
||||||
|
$nonMatchingObject2 = $this->createSimpleObject([ |
||||||
|
'active' => true, |
||||||
|
'role' => 'banned', |
||||||
|
'status' => 'active' |
||||||
|
]); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult1 = $spec->match($nonMatchingObject1); |
||||||
|
$noMatchResult2 = $spec->match($nonMatchingObject2); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult1); |
||||||
|
$this->assertFalse($noMatchResult2); |
||||||
|
} |
||||||
|
|
||||||
|
// Edge Case Tests |
||||||
|
|
||||||
|
public function testWhenFieldDoesNotExistThenReturnsFalse(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('nonExistentField', 'value')); |
||||||
|
$testObject = $this->createSimpleObject(['existingField' => 'value']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertFalse($result); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenFieldIsNullThenComparesCorrectly(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('optionalField', null)); |
||||||
|
$matchingObject = $this->createSimpleObject(['optionalField' => null]); |
||||||
|
$nonMatchingObject = $this->createSimpleObject(['optionalField' => 'value']); |
||||||
|
|
||||||
|
// When |
||||||
|
$matchResult = $spec->match($matchingObject); |
||||||
|
$noMatchResult = $spec->match($nonMatchingObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($matchResult); |
||||||
|
$this->assertFalse($noMatchResult); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingInvalidOperatorThenReturnsFalse(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(['field', 'INVALID_OP', 'value']); |
||||||
|
$testObject = $this->createSimpleObject(['field' => 'value']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertFalse($result); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingInvalidDateStringThenFallsBackToRegularComparison(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$spec = new Specification(Spec::eq('dateField', 'invalid-date')); |
||||||
|
$testObject = $this->createSimpleObject(['dateField' => 'invalid-date']); |
||||||
|
|
||||||
|
// When |
||||||
|
$result = $spec->match($testObject); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertTrue($result); |
||||||
|
} |
||||||
|
|
||||||
|
// Spec Helper Method Tests |
||||||
|
|
||||||
|
public function testWhenUsingSpecHelpersThenCreatesCorrectSpecificationArray(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$eqSpec = Spec::eq('field', 'value'); |
||||||
|
$neqSpec = Spec::neq('field', 'value'); |
||||||
|
$gtSpec = Spec::gt('field', 10); |
||||||
|
$gteSpec = Spec::gte('field', 10); |
||||||
|
$ltSpec = Spec::lt('field', 10); |
||||||
|
$lteSpec = Spec::lte('field', 10); |
||||||
|
$inSpec = Spec::in('field', ['a', 'b']); |
||||||
|
$ninSpec = Spec::nin('field', ['a', 'b']); |
||||||
|
|
||||||
|
// When & Then |
||||||
|
$this->assertEquals(['field', '=', 'value'], $eqSpec); |
||||||
|
$this->assertEquals(['field', '!=', 'value'], $neqSpec); |
||||||
|
$this->assertEquals(['field', '>', 10], $gtSpec); |
||||||
|
$this->assertEquals(['field', '>=', 10], $gteSpec); |
||||||
|
$this->assertEquals(['field', '<', 10], $ltSpec); |
||||||
|
$this->assertEquals(['field', '<=', 10], $lteSpec); |
||||||
|
$this->assertEquals(['field', 'IN', ['a', 'b']], $inSpec); |
||||||
|
$this->assertEquals(['field', 'NOT IN', ['a', 'b']], $ninSpec); |
||||||
|
} |
||||||
|
|
||||||
|
public function testWhenUsingLogicalGroupHelpersThenCreatesCorrectSpecificationArray(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$andSpec = Spec::and([Spec::eq('a', 1), Spec::eq('b', 2)]); |
||||||
|
$orSpec = Spec::or([Spec::eq('a', 1), Spec::eq('b', 2)]); |
||||||
|
$notSpec = Spec::not(Spec::eq('a', 1)); |
||||||
|
|
||||||
|
// When & Then |
||||||
|
$this->assertEquals(['AND' => [['a', '=', 1], ['b', '=', 2]]], $andSpec); |
||||||
|
$this->assertEquals(['OR' => [['a', '=', 1], ['b', '=', 2]]], $orSpec); |
||||||
|
$this->assertEquals(['NOT' => ['a', '=', 1]], $notSpec); |
||||||
|
} |
||||||
|
|
||||||
|
public function testGetSpecReturnsOriginalSpecification(): void |
||||||
|
{ |
||||||
|
// Given |
||||||
|
$originalSpec = Spec::eq('field', 'value'); |
||||||
|
$specification = new Specification($originalSpec); |
||||||
|
|
||||||
|
// When |
||||||
|
$retrievedSpec = $specification->getSpec(); |
||||||
|
|
||||||
|
// Then |
||||||
|
$this->assertEquals($originalSpec, $retrievedSpec); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Test helper class with getters for complex object testing |
||||||
|
class TestObject |
||||||
|
{ |
||||||
|
private array $data; |
||||||
|
|
||||||
|
public function __construct(array $data) |
||||||
|
{ |
||||||
|
$this->data = $data; |
||||||
|
} |
||||||
|
|
||||||
|
public function __get($name) |
||||||
|
{ |
||||||
|
return $this->data[$name] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function __isset($name) |
||||||
|
{ |
||||||
|
return isset($this->data[$name]); |
||||||
|
} |
||||||
|
|
||||||
|
public function getName() |
||||||
|
{ |
||||||
|
return $this->data['name'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getExpirationDate() |
||||||
|
{ |
||||||
|
return $this->data['expirationDate'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getCreatedAt() |
||||||
|
{ |
||||||
|
return $this->data['createdAt'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getScore() |
||||||
|
{ |
||||||
|
return $this->data['score'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getStatus() |
||||||
|
{ |
||||||
|
return $this->data['status'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getRole() |
||||||
|
{ |
||||||
|
return $this->data['role'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getAge() |
||||||
|
{ |
||||||
|
return $this->data['age'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getPrice() |
||||||
|
{ |
||||||
|
return $this->data['price'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getDepartment() |
||||||
|
{ |
||||||
|
return $this->data['department'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getActive() |
||||||
|
{ |
||||||
|
return $this->data['active'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getOptionalField() |
||||||
|
{ |
||||||
|
return $this->data['optionalField'] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
public function getDateField() |
||||||
|
{ |
||||||
|
return $this->data['dateField'] ?? null; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue