[WIP] DDD Refactoring
This commit is contained in:
parent
a3948fe32e
commit
48d3940288
@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250529155930 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create base tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<SQL
|
||||
CREATE TABLE brands (
|
||||
id VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
content JSON NOT NULL
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<SQL
|
||||
CREATE TABLE car_models (
|
||||
id VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
brand_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
content JSON NOT NULL,
|
||||
FOREIGN KEY (brand_id) REFERENCES brands(id)
|
||||
)
|
||||
SQL);
|
||||
|
||||
$this->addSql(<<<SQL
|
||||
CREATE TABLE car_revisions (
|
||||
id VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
car_model_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
content JSON NOT NULL,
|
||||
FOREIGN KEY (car_model_id) REFERENCES car_models(id)
|
||||
)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE car_revisions');
|
||||
$this->addSql('DROP TABLE car_models');
|
||||
$this->addSql('DROP TABLE brands');
|
||||
}
|
||||
}
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250530193246 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Create embeddings table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<SQL
|
||||
CREATE TABLE embeddings (
|
||||
phrase_hash VARCHAR(255) NOT NULL PRIMARY KEY,
|
||||
phrase VARCHAR(255) NOT NULL,
|
||||
large_embedding_vector VECTOR(3072),
|
||||
small_embedding_vector VECTOR(1536),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE embeddings');
|
||||
}
|
||||
}
|
||||
81
migrations/Version20250601120221.php
Normal file
81
migrations/Version20250601120221.php
Normal file
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250601120221 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add car models, revisions, properties, and embeddings tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Enable pgvector extension
|
||||
$this->addSql('CREATE EXTENSION IF NOT EXISTS vector');
|
||||
|
||||
// Create brands table
|
||||
$this->addSql('CREATE TABLE brands (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)');
|
||||
|
||||
// Create car_models table
|
||||
$this->addSql('CREATE TABLE car_models (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
brand_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)');
|
||||
|
||||
// Create car_revisions table
|
||||
$this->addSql('CREATE TABLE car_revisions (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
car_model_id VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)');
|
||||
|
||||
// Create car_properties table
|
||||
$this->addSql('CREATE TABLE car_properties (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
car_revision_id VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(255) NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)');
|
||||
|
||||
// Create embeddings table
|
||||
$this->addSql('CREATE TABLE embeddings (
|
||||
id VARCHAR(255) NOT NULL,
|
||||
phrase_hash VARCHAR(255) NOT NULL,
|
||||
phrase VARCHAR(255) NOT NULL,
|
||||
large_embedding_vector VECTOR(3072) NOT NULL,
|
||||
small_embedding_vector VECTOR(1536) NOT NULL,
|
||||
PRIMARY KEY(id)
|
||||
)');
|
||||
|
||||
// Add foreign key constraints
|
||||
$this->addSql('ALTER TABLE car_models ADD CONSTRAINT FK_car_models_brand_id FOREIGN KEY (brand_id) REFERENCES brands (id)');
|
||||
$this->addSql('ALTER TABLE car_revisions ADD CONSTRAINT FK_car_revisions_car_model_id FOREIGN KEY (car_model_id) REFERENCES car_models (id)');
|
||||
$this->addSql('ALTER TABLE car_properties ADD CONSTRAINT FK_car_properties_car_revision_id FOREIGN KEY (car_revision_id) REFERENCES car_revisions (id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE car_properties');
|
||||
$this->addSql('DROP TABLE car_revisions');
|
||||
$this->addSql('DROP TABLE car_models');
|
||||
$this->addSql('DROP TABLE embeddings');
|
||||
$this->addSql('DROP TABLE brands');
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,7 @@ use App\Domain\AI\AIClient;
|
||||
use App\Domain\Model\Persistence\PersistedEmbedding;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
use App\Domain\Model\AI\LargeEmbeddingVector;
|
||||
use App\Domain\Model\Value\Vector;
|
||||
use App\Domain\Model\Value\EmbeddingId;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
@ -41,12 +40,12 @@ class AIClientCommand extends Command
|
||||
$text = is_string($textArg) ? $textArg : '';
|
||||
|
||||
if ($input->getOption('embed')) {
|
||||
$persistedEmbedding = $this->embedText($text);
|
||||
$output->writeln($persistedEmbedding->phraseHash);
|
||||
$embedding = $this->embedText($text);
|
||||
$output->writeln($embedding->phrase);
|
||||
} else if ($input->getOption('search')) {
|
||||
$results = $this->embeddingRepository->searchByLargeEmbeddingVector(new LargeEmbeddingVector(new Vector($this->aiClient->embedText($text))));
|
||||
foreach ($results as $result) {
|
||||
$output->writeln($result->embedding->phrase);
|
||||
$results = $this->embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($text));
|
||||
foreach ($results->array() as $result) {
|
||||
$output->writeln($result->phrase);
|
||||
}
|
||||
} else {
|
||||
$output->writeln($this->aiClient->generateText($text));
|
||||
@ -55,11 +54,12 @@ class AIClientCommand extends Command
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function embedText(string $text): PersistedEmbedding
|
||||
private function embedText(string $text): Embedding
|
||||
{
|
||||
$embedding = $this->aiClient->embedText($text);
|
||||
$vector = new Vector($embedding);
|
||||
$embeddingVector = $this->aiClient->embedTextLarge($text);
|
||||
$embedding = new Embedding(EmbeddingId::generate(), $text, $embeddingVector);
|
||||
$this->embeddingRepository->save($embedding);
|
||||
|
||||
return $this->embeddingRepository->create(new Embedding($text, new LargeEmbeddingVector($vector)));
|
||||
return $embedding;
|
||||
}
|
||||
}
|
||||
@ -2,20 +2,21 @@
|
||||
|
||||
namespace App\Application\Commands;
|
||||
|
||||
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\DrivingCharacteristics;
|
||||
use App\Domain\Model\Image;
|
||||
use App\Domain\Model\Value\Date;
|
||||
use App\Domain\Model\Value\Price;
|
||||
use App\Domain\Model\Value\Currency;
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Model\Value\CarPropertyId;
|
||||
use App\Domain\Model\Battery\CellChemistry;
|
||||
use App\Domain\Model\Charging\ChargingProperties;
|
||||
use App\Domain\Model\RangeProperties;
|
||||
use App\Domain\Model\Range\WltpRange;
|
||||
use App\Domain\Model\Range\NefzRange;
|
||||
use App\Domain\Model\CarProperty;
|
||||
use App\Domain\Model\CarPropertyType;
|
||||
use App\Domain\Model\Value\Energy;
|
||||
use App\Domain\Model\Value\Power;
|
||||
use App\Domain\Model\Value\Acceleration;
|
||||
@ -25,9 +26,11 @@ use App\Domain\Model\Value\Range;
|
||||
use App\Domain\Repository\BrandRepository;
|
||||
use App\Domain\Repository\CarModelRepository;
|
||||
use App\Domain\Repository\CarRevisionRepository;
|
||||
use App\Domain\Repository\CarPropertyRepository;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
@ -37,485 +40,348 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
)]
|
||||
class LoadFixtures extends Command
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private array $brandIds = [];
|
||||
/** @var array<string, string> */
|
||||
private array $carModelIds = [];
|
||||
/** @var array<string, Brand> */
|
||||
private array $brands = [];
|
||||
/** @var array<string, CarModel> */
|
||||
private array $carModels = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly BrandRepository $brandRepository,
|
||||
private readonly CarModelRepository $carModelRepository,
|
||||
private readonly CarRevisionRepository $carRevisionRepository,
|
||||
private readonly CarPropertyRepository $carPropertyRepository,
|
||||
private readonly CarPropertyEmbedder $carPropertyEmbedder,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->addOption('delete', null, InputOption::VALUE_NONE, 'Delete all existing data before loading fixtures');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if ($input->getOption('delete') !== null) {
|
||||
$io->section('Deleting all existing data');
|
||||
$this->carPropertyRepository->deleteAll();
|
||||
$this->carRevisionRepository->deleteAll();
|
||||
$this->carModelRepository->deleteAll();
|
||||
$this->brandRepository->deleteAll();
|
||||
}
|
||||
|
||||
$io->title('Loading EV Wiki Fixtures');
|
||||
$fixtures = $this->getFixtures();
|
||||
|
||||
// Extract unique brands
|
||||
$brandNames = array_unique(array_column($fixtures, 'brand'));
|
||||
|
||||
// Load brands
|
||||
$brands = $this->getFixtureBrands();
|
||||
$io->section('Loading Brands');
|
||||
$io->progressStart(count($brands));
|
||||
$io->progressStart(count($brandNames));
|
||||
|
||||
foreach ($brands as $brand) {
|
||||
$persistedBrand = $this->brandRepository->create($brand);
|
||||
$this->brandIds[$brand->name] = $persistedBrand->id;
|
||||
foreach ($brandNames as $brandName) {
|
||||
$brand = new Brand(BrandId::generate(), $brandName);
|
||||
$this->brandRepository->save($brand);
|
||||
$this->brands[$brandName] = $brand;
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
$io->success(sprintf('Successfully loaded %d brands', count($brands)));
|
||||
$io->success(sprintf('Successfully loaded %d brands', count($brandNames)));
|
||||
|
||||
// Extract unique models
|
||||
$models = [];
|
||||
foreach ($fixtures as $brandFixture) {
|
||||
foreach ($brandFixture['models'] as $modelFixture) {
|
||||
$models[$modelFixture['model']] = $brandFixture['brand'];
|
||||
}
|
||||
}
|
||||
|
||||
// Load car models
|
||||
$carModels = $this->getFixtureCarModels();
|
||||
$io->section('Loading Car Models');
|
||||
$io->progressStart(count($carModels));
|
||||
$io->progressStart(count($models));
|
||||
|
||||
foreach ($carModels as $carModelData) {
|
||||
$model = $carModelData['model'];
|
||||
$brandName = $carModelData['brand'];
|
||||
|
||||
$persistedCarModel = $this->carModelRepository->create(
|
||||
$model,
|
||||
$this->brandIds[$brandName]
|
||||
foreach ($models as $modelName => $brandName) {
|
||||
$carModel = new CarModel(
|
||||
CarModelId::generate(),
|
||||
$this->brands[$brandName]->brandId,
|
||||
$modelName
|
||||
);
|
||||
$this->carModelIds[$model->name] = $persistedCarModel->id;
|
||||
|
||||
$this->carModelRepository->save($carModel);
|
||||
$this->carModels[$modelName] = $carModel;
|
||||
$io->progressAdvance();
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
$io->success(sprintf('Successfully loaded %d car models', count($carModels)));
|
||||
$io->success(sprintf('Successfully loaded %d car models', count($models)));
|
||||
|
||||
// Load car revisions
|
||||
$carRevisions = $this->getFixtureCarRevisions();
|
||||
$io->section('Loading Car Revisions');
|
||||
$io->progressStart(count($carRevisions));
|
||||
// Count total revisions for progress bar
|
||||
$totalRevisions = 0;
|
||||
foreach ($fixtures as $brandFixture) {
|
||||
foreach ($brandFixture['models'] as $modelFixture) {
|
||||
$totalRevisions += count($modelFixture['revisions']);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($carRevisions as $carRevisionData) {
|
||||
$revision = $carRevisionData['revision'];
|
||||
$modelName = $carRevisionData['model'];
|
||||
// Load car revisions and properties
|
||||
$io->section('Loading Car Revisions and Properties');
|
||||
$io->progressStart($totalRevisions);
|
||||
|
||||
$this->carRevisionRepository->create(
|
||||
$revision,
|
||||
$this->carModelIds[$modelName]
|
||||
foreach ($fixtures as $brandFixture) {
|
||||
$brand = $this->brands[$brandFixture['brand']];
|
||||
|
||||
foreach ($brandFixture['models'] as $modelFixture) {
|
||||
$carModel = $this->carModels[$modelFixture['model']];
|
||||
|
||||
foreach ($modelFixture['revisions'] as $revisionFixture) {
|
||||
// Create car revision
|
||||
$carRevision = new CarRevision(
|
||||
CarRevisionId::generate(),
|
||||
$carModel->carModelId,
|
||||
$revisionFixture['revision']
|
||||
);
|
||||
|
||||
$this->carRevisionRepository->save($carRevision);
|
||||
|
||||
// Create properties
|
||||
foreach ($revisionFixture['properties'] as $propertyData) {
|
||||
$property = new CarProperty(
|
||||
CarPropertyId::generate(),
|
||||
$carRevision->carRevisionId,
|
||||
$propertyData['type'],
|
||||
$propertyData['value']
|
||||
);
|
||||
$this->carPropertyRepository->save($property);
|
||||
$this->carPropertyEmbedder->createPersistedEmbedding($property, $carRevision, $brand);
|
||||
}
|
||||
|
||||
$io->progressAdvance();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
$io->success(sprintf('Successfully loaded %d car revisions', count($carRevisions)));
|
||||
|
||||
$io->success(sprintf('Successfully loaded %d car revisions with properties', $totalRevisions));
|
||||
$io->success('All fixtures loaded successfully!');
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Brand[]
|
||||
* @return array<array{brand: string, models: array<array{model: string, revisions: array<array{revision: string, properties: array<array{type: CarPropertyType, value: mixed}>}>}>}>
|
||||
*/
|
||||
private function getFixtureBrands(): array
|
||||
private function getFixtures(): array
|
||||
{
|
||||
return [
|
||||
new Brand(
|
||||
name: 'Tesla',
|
||||
logo: 'https://logo.clearbit.com/tesla.com',
|
||||
description: 'American electric vehicle and clean energy company founded by Elon Musk',
|
||||
foundedYear: 2003,
|
||||
headquarters: 'Austin, Texas, USA',
|
||||
website: 'https://tesla.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'BMW',
|
||||
logo: 'https://logo.clearbit.com/bmw.com',
|
||||
description: 'German multinational corporation producing luxury vehicles with strong EV lineup',
|
||||
foundedYear: 1916,
|
||||
headquarters: 'Munich, Germany',
|
||||
website: 'https://bmw.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Audi',
|
||||
logo: 'https://logo.clearbit.com/audi.com',
|
||||
description: 'German automotive manufacturer of luxury vehicles with e-tron electric series',
|
||||
foundedYear: 1909,
|
||||
headquarters: 'Ingolstadt, Germany',
|
||||
website: 'https://audi.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Mercedes-Benz',
|
||||
logo: 'https://logo.clearbit.com/mercedes-benz.com',
|
||||
description: 'German luxury automotive brand with EQS, EQC and other electric models',
|
||||
foundedYear: 1926,
|
||||
headquarters: 'Stuttgart, Germany',
|
||||
website: 'https://mercedes-benz.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Volkswagen',
|
||||
logo: 'https://logo.clearbit.com/vw.com',
|
||||
description: 'German motor vehicle manufacturer with ID series electric vehicles',
|
||||
foundedYear: 1937,
|
||||
headquarters: 'Wolfsburg, Germany',
|
||||
website: 'https://vw.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Porsche',
|
||||
logo: 'https://logo.clearbit.com/porsche.com',
|
||||
description: 'German sports car manufacturer with Taycan electric sports cars',
|
||||
foundedYear: 1931,
|
||||
headquarters: 'Stuttgart, Germany',
|
||||
website: 'https://porsche.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Lucid Motors',
|
||||
logo: 'https://logo.clearbit.com/lucidmotors.com',
|
||||
description: 'American electric vehicle manufacturer focused on luxury sedans',
|
||||
foundedYear: 2007,
|
||||
headquarters: 'Newark, California, USA',
|
||||
website: 'https://lucidmotors.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'Rivian',
|
||||
logo: 'https://logo.clearbit.com/rivian.com',
|
||||
description: 'American electric vehicle manufacturer focusing on electric trucks and vans',
|
||||
foundedYear: 2009,
|
||||
headquarters: 'Irvine, California, USA',
|
||||
website: 'https://rivian.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'NIO',
|
||||
logo: 'https://logo.clearbit.com/nio.com',
|
||||
description: 'Chinese electric vehicle manufacturer with innovative battery swapping technology',
|
||||
foundedYear: 2014,
|
||||
headquarters: 'Shanghai, China',
|
||||
website: 'https://nio.com',
|
||||
),
|
||||
new Brand(
|
||||
name: 'BYD',
|
||||
logo: 'https://logo.clearbit.com/byd.com',
|
||||
description: 'Chinese electric vehicle and battery manufacturer, world leader in EV sales',
|
||||
foundedYear: 1995,
|
||||
headquarters: 'Shenzhen, China',
|
||||
website: 'https://byd.com',
|
||||
)
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{brand: string, model: CarModel}>
|
||||
*/
|
||||
private function getFixtureCarModels(): array
|
||||
{
|
||||
return [
|
||||
// Tesla Models
|
||||
[
|
||||
'brand' => 'Tesla',
|
||||
'model' => new CarModel('Model S'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Tesla',
|
||||
'model' => new CarModel('Model 3'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Tesla',
|
||||
'model' => new CarModel('Model X'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Tesla',
|
||||
'model' => new CarModel('Model Y'),
|
||||
],
|
||||
|
||||
// BMW Models
|
||||
[
|
||||
'brand' => 'BMW',
|
||||
'model' => new CarModel('iX'),
|
||||
],
|
||||
[
|
||||
'brand' => 'BMW',
|
||||
'model' => new CarModel('i4'),
|
||||
],
|
||||
[
|
||||
'brand' => 'BMW',
|
||||
'model' => new CarModel('iX3'),
|
||||
],
|
||||
|
||||
// Audi Models
|
||||
[
|
||||
'brand' => 'Audi',
|
||||
'model' => new CarModel('e-tron GT'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Audi',
|
||||
'model' => new CarModel('Q4 e-tron'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Audi',
|
||||
'model' => new CarModel('e-tron'),
|
||||
],
|
||||
|
||||
// Mercedes-Benz Models
|
||||
[
|
||||
'brand' => 'Mercedes-Benz',
|
||||
'model' => new CarModel('EQS'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Mercedes-Benz',
|
||||
'model' => new CarModel('EQC'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Mercedes-Benz',
|
||||
'model' => new CarModel('EQA'),
|
||||
],
|
||||
|
||||
// Volkswagen Models
|
||||
[
|
||||
'brand' => 'Volkswagen',
|
||||
'model' => new CarModel('ID.4'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Volkswagen',
|
||||
'model' => new CarModel('ID.3'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Volkswagen',
|
||||
'model' => new CarModel('ID.Buzz'),
|
||||
],
|
||||
|
||||
// Porsche Models
|
||||
[
|
||||
'brand' => 'Porsche',
|
||||
'model' => new CarModel('Taycan'),
|
||||
],
|
||||
[
|
||||
'brand' => 'Porsche',
|
||||
'model' => new CarModel('Macan Electric'),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array{model: string, revision: CarRevision}>
|
||||
*/
|
||||
private function getFixtureCarRevisions(): array
|
||||
{
|
||||
return [
|
||||
// Tesla Model S Plaid
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'Model S',
|
||||
'revision' => new CarRevision(
|
||||
name: 'Plaid',
|
||||
productionBegin: new Date(1, 1, 2021),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(750),
|
||||
acceleration: new Acceleration(2.1),
|
||||
topSpeed: new Speed(322),
|
||||
consumption: new Consumption(new Energy(19.3))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(95.0),
|
||||
totalCapacity: new Energy(100.0),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: '4680',
|
||||
manufacturer: 'Tesla'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(250)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(628)),
|
||||
nefz: new NefzRange(new Range(652))
|
||||
),
|
||||
catalogPrice: new Price(129990, Currency::euro()),
|
||||
image: new Image('https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-S-Main-Hero-Desktop-LHD.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'Plaid',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(750)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.1)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(322)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.3))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(95.0)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(100.0)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => '4680'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(250)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(628)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(652)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(129990, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-S-Main-Hero-Desktop-LHD.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// Tesla Model 3 Long Range
|
||||
[
|
||||
'model' => 'Model 3',
|
||||
'revision' => new CarRevision(
|
||||
name: 'Long Range',
|
||||
productionBegin: new Date(1, 1, 2020),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(366),
|
||||
acceleration: new Acceleration(4.4),
|
||||
topSpeed: new Speed(233),
|
||||
consumption: new Consumption(new Energy(14.9))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(75.0),
|
||||
totalCapacity: new Energy(82.0),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: '2170',
|
||||
manufacturer: 'Tesla'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(250)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(602)),
|
||||
nefz: new NefzRange(new Range(614))
|
||||
),
|
||||
catalogPrice: new Price(49990, Currency::euro()),
|
||||
image: new Image('https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'Long Range',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2020)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(366)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.4)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(233)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(14.9))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(75.0)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(82.0)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => '2170'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(250)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(602)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(614)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(49990, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// BMW iX xDrive50
|
||||
[
|
||||
'brand' => 'BMW',
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'iX',
|
||||
'revision' => new CarRevision(
|
||||
name: 'xDrive50',
|
||||
productionBegin: new Date(1, 1, 2021),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(385),
|
||||
acceleration: new Acceleration(4.6),
|
||||
topSpeed: new Speed(200),
|
||||
consumption: new Consumption(new Energy(19.8))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(71.2),
|
||||
totalCapacity: new Energy(76.6),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'BMW Gen5',
|
||||
manufacturer: 'CATL'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(195)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(630)),
|
||||
nefz: new NefzRange(new Range(680))
|
||||
),
|
||||
catalogPrice: new Price(77300, Currency::euro()),
|
||||
image: new Image('https://www.bmw.de/content/dam/bmw/common/all-models/x-series/ix/2021/highlights/bmw-ix-sp-desktop.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'xDrive50',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(385)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.6)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(200)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.8))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(71.2)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(76.6)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'BMW Gen5'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'CATL'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(195)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(630)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(680)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(77300, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://www.bmw.de/content/dam/bmw/common/all-models/x-series/ix/2021/highlights/bmw-ix-sp-desktop.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// Audi e-tron GT RS
|
||||
[
|
||||
'brand' => 'Audi',
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'e-tron GT',
|
||||
'revision' => new CarRevision(
|
||||
name: 'RS',
|
||||
productionBegin: new Date(1, 1, 2021),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(475),
|
||||
acceleration: new Acceleration(3.3),
|
||||
topSpeed: new Speed(250),
|
||||
consumption: new Consumption(new Energy(19.6))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(83.7),
|
||||
totalCapacity: new Energy(93.4),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'PPE Platform',
|
||||
manufacturer: 'LG Energy Solution'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(270)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(472)),
|
||||
nefz: new NefzRange(new Range(487))
|
||||
),
|
||||
catalogPrice: new Price(142900, Currency::euro()),
|
||||
image: new Image('https://www.audi.de/content/dam/nemo/models/e-tron-gt/my-2021/1920x1080-gallery/1920x1080_AudiRS_e-tron_GT_19.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'RS',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(475)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(3.3)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(250)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.6))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'PPE Platform'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(270)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(472)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(487)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(142900, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://www.audi.de/content/dam/nemo/models/e-tron-gt/my-2021/1920x1080-gallery/1920x1080_AudiRS_e-tron_GT_19.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// Mercedes EQS 450+
|
||||
[
|
||||
'brand' => 'Mercedes-Benz',
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'EQS',
|
||||
'revision' => new CarRevision(
|
||||
name: '450+',
|
||||
productionBegin: new Date(1, 1, 2021),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(245),
|
||||
acceleration: new Acceleration(6.2),
|
||||
topSpeed: new Speed(210),
|
||||
consumption: new Consumption(new Energy(15.7))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(90.0),
|
||||
totalCapacity: new Energy(107.8),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'EVA Platform',
|
||||
manufacturer: 'CATL'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(200)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(756)),
|
||||
nefz: new NefzRange(new Range(770))
|
||||
),
|
||||
catalogPrice: new Price(106374, Currency::euro()),
|
||||
image: new Image('https://www.mercedes-benz.de/content/germany/de/mercedes-benz/vehicles/passenger-cars/eqs/sedan/overview/_jcr_content/root/responsivegrid/simple_stage.component.damq2.3318859008536.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => '450+',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(245)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(6.2)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(210)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(15.7))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(90.0)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(107.8)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'EVA Platform'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'CATL'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(200)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(756)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(770)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(106374, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://www.mercedes-benz.de/content/germany/de/mercedes-benz/vehicles/passenger-cars/eqs/sedan/overview/_jcr_content/root/responsivegrid/simple_stage.component.damq2.3318859008536.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// Volkswagen ID.4 Pro
|
||||
[
|
||||
'brand' => 'Volkswagen',
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'ID.4',
|
||||
'revision' => new CarRevision(
|
||||
name: 'Pro',
|
||||
productionBegin: new Date(1, 1, 2020),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(150),
|
||||
acceleration: new Acceleration(8.5),
|
||||
topSpeed: new Speed(160),
|
||||
consumption: new Consumption(new Energy(16.3))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(77.0),
|
||||
totalCapacity: new Energy(82.0),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'MEB Platform',
|
||||
manufacturer: 'LG Energy Solution'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(125)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(520)),
|
||||
nefz: new NefzRange(new Range(549))
|
||||
),
|
||||
catalogPrice: new Price(51515, Currency::euro()),
|
||||
image: new Image('https://www.volkswagen.de/content/dam/vw-ngw/vw_pkw/importers/de/models/id-4/gallery/id4-gallery-exterior-01-16x9.jpg')
|
||||
),
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'Pro',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2020)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(150)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(8.5)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(160)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(16.3))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(77.0)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(82.0)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'MEB Platform'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(125)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(520)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(549)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(51515, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://www.volkswagen.de/content/dam/vw-ngw/vw_pkw/importers/de/models/id-4/gallery/id4-gallery-exterior-01-16x9.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
|
||||
// Porsche Taycan Turbo S
|
||||
[
|
||||
'brand' => 'Porsche',
|
||||
'models' => [
|
||||
[
|
||||
'model' => 'Taycan',
|
||||
'revision' => new CarRevision(
|
||||
name: 'Turbo S',
|
||||
productionBegin: new Date(1, 1, 2019),
|
||||
drivingCharacteristics: new DrivingCharacteristics(
|
||||
power: new Power(560),
|
||||
acceleration: new Acceleration(2.8),
|
||||
topSpeed: new Speed(260),
|
||||
consumption: new Consumption(new Energy(23.7))
|
||||
),
|
||||
battery: new BatteryProperties(
|
||||
usableCapacity: new Energy(83.7),
|
||||
totalCapacity: new Energy(93.4),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'J1 Platform',
|
||||
manufacturer: 'LG Energy Solution'
|
||||
),
|
||||
chargingProperties: new ChargingProperties(
|
||||
topChargingSpeed: new Power(270)
|
||||
),
|
||||
rangeProperties: new RangeProperties(
|
||||
wltp: new WltpRange(new Range(440)),
|
||||
nefz: new NefzRange(new Range(452))
|
||||
),
|
||||
catalogPrice: new Price(185456, Currency::euro()),
|
||||
image: new Image('https://www.porsche.com/germany/models/taycan/taycan-models/turbo-s/_jcr_content/par/twocolumnlayout/par_left/image.transform.porsche-model-desktop-xl.jpg')
|
||||
),
|
||||
],
|
||||
'revisions' => [
|
||||
[
|
||||
'revision' => 'Turbo S',
|
||||
'properties' => [
|
||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2019)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(560)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.8)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(260)],
|
||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(23.7))],
|
||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)],
|
||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)],
|
||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'J1 Platform'],
|
||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'],
|
||||
['type' => CarPropertyType::CHARGING_TOP_CHARGING_SPEED, 'value' => new Power(270)],
|
||||
['type' => CarPropertyType::RANGE_WLTP, 'value' => new Range(440)],
|
||||
['type' => CarPropertyType::RANGE_NEFZ, 'value' => new Range(452)],
|
||||
['type' => CarPropertyType::CATALOG_PRICE, 'value' => new Price(185456, Currency::euro())],
|
||||
['type' => CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL, 'value' => new Image(externalPublicUrl: 'https://www.porsche.com/germany/models/taycan/taycan-models/turbo-s/_jcr_content/par/twocolumnlayout/par_left/image.transform.porsche-model-desktop-xl.jpg')]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
namespace App\Domain\AI;
|
||||
|
||||
use App\Domain\Model\AI\LargeEmbeddingVector;
|
||||
use App\Domain\Model\AI\SmallEmbeddingVector;
|
||||
use App\Domain\Model\Value\Vector;
|
||||
use OpenAI;
|
||||
|
||||
class AIClient
|
||||
@ -26,16 +29,23 @@ class AIClient
|
||||
return $response->choices[0]->message->content ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float[]
|
||||
*/
|
||||
public function embedText(string $text): array
|
||||
public function embedTextLarge(string $text): LargeEmbeddingVector
|
||||
{
|
||||
$response = $this->client->embeddings()->create([
|
||||
'model' => 'text-embedding-3-large',
|
||||
'input' => $text,
|
||||
]);
|
||||
|
||||
return $response->embeddings[0]->embedding;
|
||||
return new LargeEmbeddingVector(new Vector($response->embeddings[0]->embedding));
|
||||
}
|
||||
|
||||
public function embedTextSmall(string $text): SmallEmbeddingVector
|
||||
{
|
||||
$response = $this->client->embeddings()->create([
|
||||
'model' => 'text-embedding-3-small',
|
||||
'input' => $text,
|
||||
]);
|
||||
|
||||
return new SmallEmbeddingVector(new Vector($response->embeddings[0]->embedding));
|
||||
}
|
||||
}
|
||||
50
src/Domain/ContentManagement/CarPropertyEmbedder.php
Normal file
50
src/Domain/ContentManagement/CarPropertyEmbedder.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\ContentManagement;
|
||||
|
||||
use App\Domain\AI\AIClient;
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\CarProperty;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Persistence\PersistedEmbedding;
|
||||
use App\Domain\Model\Value\EmbeddingId;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
use Stringable;
|
||||
|
||||
class CarPropertyEmbedder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AIClient $aiClient,
|
||||
private readonly EmbeddingRepository $embeddingRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?Brand $brand): ?Embedding
|
||||
{
|
||||
if (!($carProperty->value instanceof Stringable)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = $carProperty->type->humanReadable() . ': ' . (string) $carProperty->value;
|
||||
if ($carRevision !== null) {
|
||||
$text .= ' - ' . $carRevision->name;
|
||||
}
|
||||
if ($brand !== null) {
|
||||
$text .= ', ' . $brand->name;
|
||||
}
|
||||
|
||||
$persistedEmbedding = $this->embeddingRepository->findByPhrase($text);
|
||||
if ($persistedEmbedding) {
|
||||
return $persistedEmbedding;
|
||||
}
|
||||
|
||||
$largeEmbedding = $this->aiClient->embedTextLarge($text);
|
||||
$smallEmbedding = $this->aiClient->embedTextSmall($text);
|
||||
|
||||
$embedding = new Embedding(EmbeddingId::generate(), $text, $largeEmbedding, $smallEmbedding);
|
||||
$this->embeddingRepository->save($embedding);
|
||||
|
||||
return $embedding;
|
||||
}
|
||||
}
|
||||
@ -2,9 +2,12 @@
|
||||
|
||||
namespace App\Domain\Model\AI;
|
||||
|
||||
use App\Domain\Model\Value\EmbeddingId;
|
||||
|
||||
class Embedding
|
||||
{
|
||||
public function __construct(
|
||||
public readonly EmbeddingId $embeddingId,
|
||||
public readonly string $phrase,
|
||||
public readonly ?LargeEmbeddingVector $largeEmbeddingVector = null,
|
||||
public readonly ?SmallEmbeddingVector $smallEmbeddingVector = null,
|
||||
@ -13,4 +16,9 @@ class Embedding
|
||||
throw new \InvalidArgumentException('At least one embedding vector must be provided');
|
||||
}
|
||||
}
|
||||
|
||||
public function phraseHash(): string
|
||||
{
|
||||
return hash('sha256', $this->phrase);
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Domain\Model\Battery;
|
||||
|
||||
use Stringable;
|
||||
|
||||
enum CellChemistry: string
|
||||
{
|
||||
case LithiumIronPhosphate = 'LFP';
|
||||
|
||||
@ -2,14 +2,13 @@
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
|
||||
final readonly class Brand
|
||||
{
|
||||
public function __construct(
|
||||
public readonly BrandId $brandId,
|
||||
public readonly string $name,
|
||||
public readonly ?string $logo = null,
|
||||
public readonly ?string $description = null,
|
||||
public readonly ?int $foundedYear = null,
|
||||
public readonly ?string $headquarters = null,
|
||||
public readonly ?string $website = null,
|
||||
public readonly ?EmbeddingCollection $embeddings = null,
|
||||
) {}
|
||||
}
|
||||
@ -2,10 +2,14 @@
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
|
||||
class CarModel
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?Brand $brand = null,
|
||||
public readonly CarModelId $carModelId,
|
||||
public readonly BrandId $brandId,
|
||||
public string $name
|
||||
) {}
|
||||
}
|
||||
16
src/Domain/Model/CarProperty.php
Normal file
16
src/Domain/Model/CarProperty.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Value\CarPropertyId;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
|
||||
final readonly class CarProperty
|
||||
{
|
||||
public function __construct(
|
||||
public readonly CarPropertyId $carPropertyId,
|
||||
public readonly CarRevisionId $carRevisionId,
|
||||
public CarPropertyType $type,
|
||||
public mixed $value,
|
||||
) {}
|
||||
}
|
||||
55
src/Domain/Model/CarPropertyCollection.php
Normal file
55
src/Domain/Model/CarPropertyCollection.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
final class CarPropertyCollection
|
||||
{
|
||||
/**
|
||||
* @param CarProperty[] $properties
|
||||
*/
|
||||
public function __construct(
|
||||
private array $properties = [],
|
||||
) {}
|
||||
|
||||
public function add(CarProperty $property): void
|
||||
{
|
||||
$this->properties[] = $property;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CarProperty[]
|
||||
*/
|
||||
public function array(): array
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
public function get(CarPropertyType $type): CarPropertyCollection
|
||||
{
|
||||
return new CarPropertyCollection(array_filter($this->properties, fn(CarProperty $property) => $property->type === $type));
|
||||
}
|
||||
|
||||
public function getOne(CarPropertyType $type): ?CarProperty
|
||||
{
|
||||
return array_values($this->get($type)->array())[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CarPropertyType[] $types
|
||||
*/
|
||||
public function hasTypes(array $types): bool
|
||||
{
|
||||
foreach ($types as $type) {
|
||||
if ($this->get($type)->count() === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
101
src/Domain/Model/CarPropertyType.php
Normal file
101
src/Domain/Model/CarPropertyType.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
enum CarPropertyType: string
|
||||
{
|
||||
case NAME = 'name';
|
||||
case PRODUCTION_BEGIN = 'production_begin';
|
||||
case PRODUCTION_END = 'production_end';
|
||||
|
||||
// DrivingCharacteristics fields
|
||||
case DRIVING_CHARACTERISTICS_POWER = 'driving_characteristics_power';
|
||||
case DRIVING_CHARACTERISTICS_ACCELERATION = 'driving_characteristics_acceleration';
|
||||
case DRIVING_CHARACTERISTICS_TOP_SPEED = 'driving_characteristics_top_speed';
|
||||
case DRIVING_CHARACTERISTICS_CONSUMPTION = 'driving_characteristics_consumption';
|
||||
|
||||
// BatteryProperties fields
|
||||
case BATTERY_USABLE_CAPACITY = 'battery_usable_capacity';
|
||||
case BATTERY_TOTAL_CAPACITY = 'battery_total_capacity';
|
||||
case BATTERY_CELL_CHEMISTRY = 'battery_cell_chemistry';
|
||||
case BATTERY_MODEL = 'battery_model';
|
||||
case BATTERY_MANUFACTURER = 'battery_manufacturer';
|
||||
|
||||
// ChargingProperties fields
|
||||
case CHARGING_TOP_CHARGING_SPEED = 'charging_top_charging_speed';
|
||||
case CHARGING_CHARGE_CURVE = 'charging_charge_curve';
|
||||
case CHARGING_CHARGE_TIME_PROPERTIES = 'charging_charge_time_properties';
|
||||
case CHARGING_CONNECTIVITY = 'charging_connectivity';
|
||||
|
||||
// ChargeTimeProperties fields
|
||||
case CHARGE_TIME_0_TO_100 = 'charge_time_0_to_100';
|
||||
case CHARGE_TIME_0_TO_70 = 'charge_time_0_to_70';
|
||||
case CHARGE_TIME_10_TO_70 = 'charge_time_10_to_70';
|
||||
case CHARGE_TIME_20_TO_70 = 'charge_time_20_to_70';
|
||||
case CHARGE_TIME_10_TO_80 = 'charge_time_10_to_80';
|
||||
case CHARGE_TIME_20_TO_80 = 'charge_time_20_to_80';
|
||||
case CHARGE_TIME_10_TO_90 = 'charge_time_10_to_90';
|
||||
case CHARGE_TIME_20_TO_90 = 'charge_time_20_to_90';
|
||||
|
||||
// ChargingConnectivity fields
|
||||
case CHARGING_IS_400V = 'charging_is_400v';
|
||||
case CHARGING_IS_800V = 'charging_is_800v';
|
||||
case CHARGING_PLUG_AND_CHARGE = 'charging_plug_and_charge';
|
||||
case CHARGING_CONNECTOR_TYPES = 'charging_connector_types';
|
||||
|
||||
// RangeProperties fields
|
||||
case RANGE_WLTP = 'range_wltp';
|
||||
case RANGE_NEFZ = 'range_nefz';
|
||||
case RANGE_REAL_RANGE_TESTS = 'range_real_range_tests';
|
||||
|
||||
// Price fields
|
||||
case CATALOG_PRICE = 'catalog_price';
|
||||
case CATALOG_PRICE_CURRENCY = 'catalog_price_currency';
|
||||
case CATALOG_PRICE_INCLUDES_VAT = 'catalog_price_includes_vat';
|
||||
|
||||
// Image fields
|
||||
case IMAGE_EXTERNAL_PUBLIC_URL = 'image_external_public_url';
|
||||
case IMAGE_RELATIVE_PUBLIC_URL = 'image_relative_public_url';
|
||||
|
||||
public function humanReadable(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::NAME => 'Name',
|
||||
self::PRODUCTION_BEGIN => 'Production Begin',
|
||||
self::PRODUCTION_END => 'Production End',
|
||||
self::DRIVING_CHARACTERISTICS_POWER => 'Power',
|
||||
self::DRIVING_CHARACTERISTICS_ACCELERATION => 'Acceleration',
|
||||
self::DRIVING_CHARACTERISTICS_TOP_SPEED => 'Top Speed',
|
||||
self::DRIVING_CHARACTERISTICS_CONSUMPTION => 'Consumption',
|
||||
self::BATTERY_USABLE_CAPACITY => 'Usable Capacity',
|
||||
self::BATTERY_TOTAL_CAPACITY => 'Total Capacity',
|
||||
self::BATTERY_CELL_CHEMISTRY => 'Cell Chemistry',
|
||||
self::BATTERY_MODEL => 'Battery Model',
|
||||
self::BATTERY_MANUFACTURER => 'Battery Manufacturer',
|
||||
self::CHARGING_TOP_CHARGING_SPEED => 'Top Charging Speed',
|
||||
self::CHARGING_CHARGE_CURVE => 'Charge Curve',
|
||||
self::CHARGING_CHARGE_TIME_PROPERTIES => 'Charge Time Properties',
|
||||
self::CHARGING_CONNECTIVITY => 'Charging Connectivity',
|
||||
self::CHARGING_CONNECTOR_TYPES => 'Connector Types',
|
||||
self::CHARGING_PLUG_AND_CHARGE => 'Charging Plug and Charge',
|
||||
self::RANGE_WLTP => 'WLTP Range',
|
||||
self::RANGE_NEFZ => 'NEFZ Range',
|
||||
self::RANGE_REAL_RANGE_TESTS => 'Real Range Tests',
|
||||
self::CATALOG_PRICE => 'Catalog Price',
|
||||
self::CATALOG_PRICE_CURRENCY => 'Catalog Price Currency',
|
||||
self::CATALOG_PRICE_INCLUDES_VAT => 'Catalog Price Includes VAT',
|
||||
self::IMAGE_EXTERNAL_PUBLIC_URL => 'External Public URL',
|
||||
self::IMAGE_RELATIVE_PUBLIC_URL => 'Relative Public URL',
|
||||
self::CHARGE_TIME_0_TO_100 => 'Charge Time 0 to 100',
|
||||
self::CHARGE_TIME_0_TO_70 => 'Charge Time 0 to 70',
|
||||
self::CHARGE_TIME_10_TO_70 => 'Charge Time 10 to 70',
|
||||
self::CHARGE_TIME_20_TO_70 => 'Charge Time 20 to 70',
|
||||
self::CHARGE_TIME_10_TO_80 => 'Charge Time 10 to 80',
|
||||
self::CHARGE_TIME_20_TO_80 => 'Charge Time 20 to 80',
|
||||
self::CHARGE_TIME_10_TO_90 => 'Charge Time 10 to 90',
|
||||
self::CHARGE_TIME_20_TO_90 => 'Charge Time 20 to 90',
|
||||
self::CHARGING_IS_400V => 'Charging is 400V',
|
||||
self::CHARGING_IS_800V => 'Charging is 800V',
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -2,31 +2,14 @@
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Value\Consumption;
|
||||
use App\Domain\Model\Value\Date;
|
||||
use App\Domain\Model\Value\Price;
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
use App\Domain\Model\Charging\ChargingProperties;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
|
||||
final readonly class CarRevision
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public ?Date $productionBegin = null,
|
||||
public ?Date $productionEnd = null,
|
||||
public ?DrivingCharacteristics $drivingCharacteristics = null,
|
||||
public ?BatteryProperties $battery = null,
|
||||
public ?ChargingProperties $chargingProperties = null,
|
||||
public ?RangeProperties $rangeProperties = null,
|
||||
|
||||
public ?Price $catalogPrice = null,
|
||||
public ?CarModel $carModel = null,
|
||||
public ?Image $image = null,
|
||||
) {
|
||||
if ($this->productionBegin && $this->productionEnd) {
|
||||
if ($this->productionBegin->year > $this->productionEnd->year) {
|
||||
throw new \InvalidArgumentException('Production begin year must be before production end year');
|
||||
}
|
||||
}
|
||||
}
|
||||
public readonly CarRevisionId $carRevisionId,
|
||||
public readonly CarModelId $carModelId,
|
||||
public readonly string $name,
|
||||
) {}
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Value\Power;
|
||||
use App\Domain\Model\Value\Acceleration;
|
||||
use App\Domain\Model\Value\Consumption;
|
||||
use App\Domain\Model\Value\Speed;
|
||||
|
||||
final readonly class DrivingCharacteristics
|
||||
{
|
||||
public function __construct(
|
||||
public ?Power $power = null,
|
||||
public ?Acceleration $acceleration = null,
|
||||
public ?Speed $topSpeed = null,
|
||||
public ?Consumption $consumption = null,
|
||||
) {}
|
||||
}
|
||||
23
src/Domain/Model/EmbeddingCollection.php
Normal file
23
src/Domain/Model/EmbeddingCollection.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
|
||||
class EmbeddingCollection
|
||||
{
|
||||
/**
|
||||
* @param Embedding[] $embeddings
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly array $embeddings,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return Embedding[]
|
||||
*/
|
||||
public function array(): array
|
||||
{
|
||||
return $this->embeddings;
|
||||
}
|
||||
}
|
||||
@ -2,11 +2,18 @@
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
final readonly class Image
|
||||
use Stringable;
|
||||
|
||||
final readonly class Image implements Stringable
|
||||
{
|
||||
public function __construct(
|
||||
public ?string $externalPublicUrl = null,
|
||||
public ?string $relativePublicUrl = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->externalPublicUrl ?? $this->relativePublicUrl ?? '';
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Persistence;
|
||||
|
||||
use App\Domain\Model\Brand;
|
||||
|
||||
class PersistedBrand
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly Brand $brand,
|
||||
) {}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Persistence;
|
||||
|
||||
use App\Domain\Model\CarModel;
|
||||
|
||||
class PersistedCarModel
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly CarModel $carModel,
|
||||
) {}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Persistence;
|
||||
|
||||
use App\Domain\Model\CarRevision;
|
||||
|
||||
class PersistedCarRevision
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly CarRevision $carRevision,
|
||||
) {}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Persistence;
|
||||
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
|
||||
class PersistedEmbedding
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $phraseHash,
|
||||
public readonly Embedding $embedding,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
|
||||
use App\Domain\Model\Range\NefzRange;
|
||||
use App\Domain\Model\Range\WltpRange;
|
||||
use App\Domain\Model\Range\RealRange;
|
||||
|
||||
final readonly class RangeProperties
|
||||
{
|
||||
/**
|
||||
* @param RealRange[] $realRangeTests
|
||||
*/
|
||||
public function __construct(
|
||||
public readonly ?WltpRange $wltp = null,
|
||||
public readonly ?NefzRange $nefz = null,
|
||||
public readonly array $realRangeTests = [],
|
||||
) {}
|
||||
}
|
||||
29
src/Domain/Model/Value/BrandId.php
Normal file
29
src/Domain/Model/Value/BrandId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
final readonly class BrandId
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!str_starts_with($value, 'brand_')) {
|
||||
throw new \InvalidArgumentException('BrandId must start with "brand_"');
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(BrandId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public static function generate(): BrandId
|
||||
{
|
||||
return new BrandId(uniqid('brand_', true));
|
||||
}
|
||||
}
|
||||
29
src/Domain/Model/Value/CarModelId.php
Normal file
29
src/Domain/Model/Value/CarModelId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
final readonly class CarModelId
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!str_starts_with($value, 'carmodel_')) {
|
||||
throw new \InvalidArgumentException('CarModelId must start with "carmodel_"');
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(CarModelId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public static function generate(): CarModelId
|
||||
{
|
||||
return new CarModelId(uniqid('carmodel_', true));
|
||||
}
|
||||
}
|
||||
29
src/Domain/Model/Value/CarPropertyId.php
Normal file
29
src/Domain/Model/Value/CarPropertyId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
final readonly class CarPropertyId
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!str_starts_with($value, 'carproperty_')) {
|
||||
throw new \InvalidArgumentException('CarPropertyId must start with "carproperty_"');
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(CarPropertyId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public static function generate(): CarPropertyId
|
||||
{
|
||||
return new CarPropertyId(uniqid('carproperty_', true));
|
||||
}
|
||||
}
|
||||
29
src/Domain/Model/Value/CarRevisionId.php
Normal file
29
src/Domain/Model/Value/CarRevisionId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
final readonly class CarRevisionId
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!str_starts_with($value, 'carrevision_')) {
|
||||
throw new \InvalidArgumentException('CarRevisionId must start with "carrevision_"');
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(CarRevisionId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public static function generate(): CarRevisionId
|
||||
{
|
||||
return new CarRevisionId(uniqid('carrevision_', true));
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
|
||||
class Consumption
|
||||
{
|
||||
public function __construct(
|
||||
|
||||
29
src/Domain/Model/Value/EmbeddingId.php
Normal file
29
src/Domain/Model/Value/EmbeddingId.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model\Value;
|
||||
|
||||
final readonly class EmbeddingId
|
||||
{
|
||||
public function __construct(
|
||||
public string $value
|
||||
) {
|
||||
if (!str_starts_with($value, 'embedding_')) {
|
||||
throw new \InvalidArgumentException('EmbeddingId must start with "embedding_"');
|
||||
}
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function equals(EmbeddingId $other): bool
|
||||
{
|
||||
return $this->value === $other->value;
|
||||
}
|
||||
|
||||
public static function generate(): EmbeddingId
|
||||
{
|
||||
return new EmbeddingId(uniqid('embedding_', true));
|
||||
}
|
||||
}
|
||||
@ -4,13 +4,18 @@ namespace App\Domain\Repository;
|
||||
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\BrandCollection;
|
||||
use App\Domain\Model\Persistence\PersistedBrand;
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
|
||||
interface BrandRepository
|
||||
{
|
||||
public function findAll(): BrandCollection;
|
||||
|
||||
public function create(Brand $brand): PersistedBrand;
|
||||
public function findByCarModel(CarModel $carModel): ?Brand;
|
||||
|
||||
public function update(PersistedBrand $persistedBrand): void;
|
||||
public function findById(BrandId $brandId): ?Brand;
|
||||
|
||||
public function save(Brand $brand): void;
|
||||
|
||||
public function deleteAll(): void;
|
||||
}
|
||||
@ -4,19 +4,23 @@ namespace App\Domain\Repository;
|
||||
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\CarModelCollection;
|
||||
use App\Domain\Model\Persistence\PersistedCarModel;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
|
||||
interface CarModelRepository
|
||||
{
|
||||
public function findAll(): CarModelCollection;
|
||||
|
||||
public function findById(string $id): ?PersistedCarModel;
|
||||
public function findById(CarModelId $carModelId): ?CarModel;
|
||||
|
||||
public function findByBrandId(string $brandId): CarModelCollection;
|
||||
public function findByBrandId(BrandId $brandId): CarModelCollection;
|
||||
|
||||
public function create(CarModel $carModel, string $brandId): PersistedCarModel;
|
||||
public function findByCarRevision(CarRevision $carRevision): ?CarModel;
|
||||
|
||||
public function update(PersistedCarModel $persistedCarModel): void;
|
||||
public function save(CarModel $carModel): void;
|
||||
|
||||
public function delete(PersistedCarModel $persistedCarModel): void;
|
||||
public function delete(CarModel $carModel): void;
|
||||
|
||||
public function deleteAll(): void;
|
||||
}
|
||||
30
src/Domain/Repository/CarPropertyRepository.php
Normal file
30
src/Domain/Repository/CarPropertyRepository.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Repository;
|
||||
|
||||
use App\Domain\Model\CarProperty;
|
||||
use App\Domain\Model\CarPropertyCollection;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Model\Value\EmbeddingId;
|
||||
|
||||
interface CarPropertyRepository
|
||||
{
|
||||
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection;
|
||||
|
||||
/**
|
||||
* @param EmbeddingId[] $embeddingIds
|
||||
*/
|
||||
public function findByEmbeddingIds(array $embeddingIds): CarPropertyCollection;
|
||||
|
||||
/**
|
||||
* @param string[] $phraseHashes
|
||||
*/
|
||||
public function findByEmbeddingPhraseHashes(array $phraseHashes): CarPropertyCollection;
|
||||
|
||||
public function save(CarProperty $carProperty): void;
|
||||
|
||||
public function delete(CarProperty $carProperty): void;
|
||||
|
||||
public function deleteAll(): void;
|
||||
}
|
||||
@ -4,19 +4,20 @@ namespace App\Domain\Repository;
|
||||
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\CarRevisionCollection;
|
||||
use App\Domain\Model\Persistence\PersistedCarRevision;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
|
||||
interface CarRevisionRepository
|
||||
{
|
||||
public function findAll(): CarRevisionCollection;
|
||||
|
||||
public function findById(string $id): ?PersistedCarRevision;
|
||||
public function findById(CarRevisionId $carRevisionId): ?CarRevision;
|
||||
|
||||
public function findByCarModelId(string $carModelId): CarRevisionCollection;
|
||||
public function findByCarModelId(CarModelId $carModelId): CarRevisionCollection;
|
||||
|
||||
public function create(CarRevision $carRevision, string $carModelId): PersistedCarRevision;
|
||||
public function save(CarRevision $carRevision): void;
|
||||
|
||||
public function update(PersistedCarRevision $persistedCarRevision): void;
|
||||
public function delete(CarRevision $carRevision): void;
|
||||
|
||||
public function delete(PersistedCarRevision $persistedCarRevision): void;
|
||||
public function deleteAll(): void;
|
||||
}
|
||||
@ -5,26 +5,31 @@ namespace App\Domain\Repository;
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
use App\Domain\Model\AI\LargeEmbeddingVector;
|
||||
use App\Domain\Model\AI\SmallEmbeddingVector;
|
||||
use App\Domain\Model\Persistence\PersistedEmbedding;
|
||||
use App\Domain\Model\Value\Vector;
|
||||
use App\Domain\Model\EmbeddingCollection;
|
||||
|
||||
interface EmbeddingRepository
|
||||
{
|
||||
public function create(Embedding $embedding): PersistedEmbedding;
|
||||
public function save(Embedding $embedding): void;
|
||||
|
||||
public function delete(PersistedEmbedding $persistedEmbedding): void;
|
||||
public function delete(Embedding $embedding): void;
|
||||
|
||||
/**
|
||||
* @param LargeEmbeddingVector $embeddingVector
|
||||
* @param int $limit
|
||||
* @return PersistedEmbedding[]
|
||||
* @return EmbeddingCollection
|
||||
*/
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): array;
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): EmbeddingCollection;
|
||||
|
||||
/**
|
||||
* @param SmallEmbeddingVector $vector
|
||||
* @param int $limit
|
||||
* @return PersistedEmbedding[]
|
||||
* @return EmbeddingCollection
|
||||
*/
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 10): array;
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 10): EmbeddingCollection;
|
||||
|
||||
/**
|
||||
* @param string $phrase
|
||||
* @return Embedding|null
|
||||
*/
|
||||
public function findByPhrase(string $phrase): ?Embedding;
|
||||
}
|
||||
@ -2,169 +2,27 @@
|
||||
|
||||
namespace App\Domain\Search;
|
||||
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\DrivingCharacteristics;
|
||||
use App\Domain\Model\Image;
|
||||
use App\Domain\Model\Value\Currency;
|
||||
use App\Domain\Model\Value\Date;
|
||||
use App\Domain\Model\Value\Price;
|
||||
use App\Domain\Model\Value\Range;
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
use App\Domain\Model\Battery\CellChemistry;
|
||||
use App\Domain\Model\Value\Power;
|
||||
use App\Domain\Model\Value\Acceleration;
|
||||
use App\Domain\Model\Value\Consumption;
|
||||
use App\Domain\Model\Value\Speed;
|
||||
use App\Domain\Model\Value\Drivetrain;
|
||||
use App\Domain\Model\Value\Energy;
|
||||
use App\Domain\Model\Value\ChargingSpeed;
|
||||
use App\Domain\Model\Charging\ChargingProperties;
|
||||
use App\Domain\Model\Charging\ChargeCurve;
|
||||
use App\Domain\Model\Charging\ChargeTimeProperties;
|
||||
use App\Domain\Model\Charging\ChargingConnectivity;
|
||||
use App\Domain\Model\Charging\ConnectorType;
|
||||
use App\Domain\Model\RangeProperties;
|
||||
use App\Domain\Model\Range\WltpRange;
|
||||
use App\Domain\Model\Range\NefzRange;
|
||||
use App\Domain\Model\Range\RealRange;
|
||||
use App\Domain\Model\Value\Season;
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
use App\Domain\Search\Tiles\SubSectionTile;
|
||||
use App\Domain\Search\Tiles\BrandTile;
|
||||
use App\Domain\Search\Tiles\PriceTile;
|
||||
use App\Domain\Search\Tiles\RangeTile;
|
||||
use App\Domain\Search\Tiles\BatteryTile;
|
||||
use App\Domain\Search\Tiles\PowerTile;
|
||||
use App\Domain\Search\Tiles\AccelerationTile;
|
||||
use App\Domain\Search\Tiles\ChargingTile;
|
||||
use App\Domain\Search\Tiles\ConsumptionTile;
|
||||
use App\Domain\Search\Tiles\AvailabilityTile;
|
||||
use App\Domain\Search\Tiles\CarTile;
|
||||
use App\Domain\Search\Tiles\TopSpeedTile;
|
||||
use App\Domain\Search\Tiles\DrivetrainTile;
|
||||
use App\Domain\Search\Tiles\ChargeTimeTile;
|
||||
use App\Domain\Search\Tiles\ChargingConnectivityTile;
|
||||
use App\Domain\Search\Tiles\BatteryDetailsTile;
|
||||
use App\Domain\Search\Tiles\RealRangeTile;
|
||||
use App\Domain\AI\AIClient;
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
use App\Domain\Repository\CarPropertyRepository;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
|
||||
class Engine
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EmbeddingRepository $embeddingRepository,
|
||||
private readonly CarPropertyRepository $carPropertyRepository,
|
||||
private readonly AIClient $aiClient,
|
||||
private readonly TileBuilder $tileBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
public function search(string $query): TileCollection
|
||||
{
|
||||
$batteryProperties = new BatteryProperties(
|
||||
usableCapacity: new Energy(77.0),
|
||||
totalCapacity: new Energy(82.0),
|
||||
cellChemistry: CellChemistry::LithiumNickelManganeseOxide,
|
||||
model: 'NCM811',
|
||||
manufacturer: 'LG Energy Solution'
|
||||
);
|
||||
$results = $this->embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($query));
|
||||
|
||||
$chargeCurve = new ChargeCurve(
|
||||
averagePowerSoc0: new Power(175),
|
||||
averagePowerSoc10: new Power(175),
|
||||
averagePowerSoc20: new Power(170),
|
||||
averagePowerSoc30: new Power(165),
|
||||
averagePowerSoc40: new Power(155),
|
||||
averagePowerSoc50: new Power(145),
|
||||
averagePowerSoc60: new Power(130),
|
||||
averagePowerSoc70: new Power(110),
|
||||
averagePowerSoc80: new Power(85),
|
||||
averagePowerSoc90: new Power(50),
|
||||
averagePowerSoc100: new Power(20)
|
||||
);
|
||||
$carProperties = $this->carPropertyRepository->findByEmbeddingPhraseHashes(array_map(fn (Embedding $embedding) => $embedding->phraseHash(), $results->array()));
|
||||
|
||||
$chargeTimeProperties = new ChargeTimeProperties(
|
||||
minutesFrom0To100: 155,
|
||||
minutesFrom10To80: 28,
|
||||
minutesFrom20To80: 25,
|
||||
minutesFrom10To90: 42
|
||||
);
|
||||
|
||||
$chargingConnectivity = new ChargingConnectivity(
|
||||
is400v: true,
|
||||
is800v: false,
|
||||
plugAndCharge: true,
|
||||
connectorTypes: [ConnectorType::CCS, ConnectorType::Type2]
|
||||
);
|
||||
|
||||
$chargingProperties = new ChargingProperties(
|
||||
topChargingSpeed: new Power(175),
|
||||
chargeCurve: $chargeCurve,
|
||||
chargeTimeProperties: $chargeTimeProperties,
|
||||
chargingConnectivity: $chargingConnectivity
|
||||
);
|
||||
|
||||
$drivingCharacteristics = new DrivingCharacteristics(
|
||||
power: new Power(210),
|
||||
acceleration: new Acceleration(6.6),
|
||||
topSpeed: new Speed(180),
|
||||
consumption: new Consumption(new Energy(17.1))
|
||||
);
|
||||
|
||||
$wltpRange = new WltpRange(new Range(450));
|
||||
$nefzRange = new NefzRange(new Range(485));
|
||||
$realRangeTests = [
|
||||
new RealRange(new Range(380), Season::Winter, new Speed(130)),
|
||||
new RealRange(new Range(420), Season::Summer, new Speed(120)),
|
||||
new RealRange(new Range(365), Season::Winter, new Speed(160))
|
||||
];
|
||||
|
||||
$rangeProperties = new RangeProperties(
|
||||
wltp: $wltpRange,
|
||||
nefz: $nefzRange,
|
||||
realRangeTests: $realRangeTests
|
||||
);
|
||||
|
||||
$skodaElroq85 = new CarRevision(
|
||||
name: 'Skoda Enyaq iV 85',
|
||||
productionBegin: new Date(1, 1, 2020),
|
||||
productionEnd: null,
|
||||
drivingCharacteristics: $drivingCharacteristics,
|
||||
battery: $batteryProperties,
|
||||
chargingProperties: $chargingProperties,
|
||||
rangeProperties: $rangeProperties,
|
||||
catalogPrice: new Price(43900, Currency::euro()),
|
||||
image: new Image('https://www.scherer-gruppe.de/media/f3b72d42-4b26-4606-8df4-d840efeff017/01_elroc.jpg?w=1920&h=758&action=crop&scale=both&anchor=middlecenter')
|
||||
);
|
||||
|
||||
$chargingSpeed = new ChargingSpeed(
|
||||
dcMaxKw: new Power(175),
|
||||
acMaxKw: new Power(11)
|
||||
);
|
||||
|
||||
return new TileCollection([
|
||||
new SectionTile('Skoda Enyaq iV 85', [
|
||||
new CarTile($skodaElroq85->image ?? new Image(), array_filter([
|
||||
new BrandTile('Skoda'),
|
||||
$skodaElroq85->catalogPrice ? new PriceTile($skodaElroq85->catalogPrice) : null,
|
||||
new AvailabilityTile('Verfügbar', new Date(1, 1, 2020)),
|
||||
new RangeTile($wltpRange->range),
|
||||
$drivingCharacteristics->consumption ? new ConsumptionTile($drivingCharacteristics->consumption) : null,
|
||||
$drivingCharacteristics->acceleration ? new AccelerationTile($drivingCharacteristics->acceleration) : null,
|
||||
])),
|
||||
|
||||
new SubSectionTile('Performance', array_filter([
|
||||
$drivingCharacteristics->power ? new PowerTile($drivingCharacteristics->power) : null,
|
||||
$drivingCharacteristics->topSpeed ? new TopSpeedTile($drivingCharacteristics->topSpeed) : null,
|
||||
new DrivetrainTile(new Drivetrain('rear')),
|
||||
])),
|
||||
|
||||
new SubSectionTile('Reichweite', [
|
||||
new RangeTile($wltpRange->range),
|
||||
new RealRangeTile($realRangeTests),
|
||||
]),
|
||||
|
||||
new SubSectionTile('Batterie', array_filter([
|
||||
$skodaElroq85->battery ? new BatteryTile($skodaElroq85->battery) : null,
|
||||
$skodaElroq85->battery ? new BatteryDetailsTile($skodaElroq85->battery) : null,
|
||||
])),
|
||||
|
||||
new SubSectionTile('Laden', array_filter([
|
||||
new ChargingTile($chargingSpeed),
|
||||
$chargingProperties->chargeTimeProperties ? new ChargeTimeTile($chargingProperties->chargeTimeProperties) : null,
|
||||
$chargingProperties->chargingConnectivity ? new ChargingConnectivityTile($chargingProperties->chargingConnectivity) : null,
|
||||
])),
|
||||
]),
|
||||
]);
|
||||
return $this->tileBuilder->build($carProperties);
|
||||
}
|
||||
}
|
||||
58
src/Domain/Search/TileBuilder.php
Normal file
58
src/Domain/Search/TileBuilder.php
Normal file
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search;
|
||||
|
||||
use App\Domain\Model\CarPropertyCollection;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Repository\BrandRepository;
|
||||
use App\Domain\Repository\CarModelRepository;
|
||||
use App\Domain\Repository\CarRevisionRepository;
|
||||
use App\Domain\Search\TileBuilders\CarTileBuilder;
|
||||
|
||||
class TileBuilder
|
||||
{
|
||||
private readonly CarTileBuilder $carTileBuilder;
|
||||
|
||||
public function __construct(
|
||||
private readonly CarRevisionRepository $carRevisionRepository,
|
||||
private readonly CarModelRepository $carModelRepository,
|
||||
private readonly BrandRepository $brandRepository,
|
||||
) {
|
||||
$this->carTileBuilder = new CarTileBuilder();
|
||||
}
|
||||
|
||||
public function build(CarPropertyCollection $carProperties): TileCollection
|
||||
{
|
||||
$carRevisionGroups = [];
|
||||
foreach ($carProperties->array() as $carProperty) {
|
||||
$carRevisionId = $carProperty->carRevisionId->value;
|
||||
if (!isset($carRevisionGroups[$carRevisionId])) {
|
||||
$carRevisionGroups[$carRevisionId] = new CarPropertyCollection();
|
||||
}
|
||||
$carRevisionGroups[$carRevisionId]->add($carProperty);
|
||||
}
|
||||
|
||||
$tiles = new TileCollection([]);
|
||||
|
||||
foreach ($carRevisionGroups as $carRevisionId => $properties) {
|
||||
$persistedCarRevision = $this->carRevisionRepository->findById(new CarRevisionId($carRevisionId));
|
||||
if ($persistedCarRevision === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$carModel = $this->carModelRepository->findByCarRevision($persistedCarRevision);
|
||||
if ($carModel === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$brand = $this->brandRepository->findByCarModel($carModel);
|
||||
if ($brand === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->carTileBuilder->build($brand, $carModel, $persistedCarRevision, $properties, $tiles);
|
||||
}
|
||||
|
||||
return $tiles;
|
||||
}
|
||||
}
|
||||
67
src/Domain/Search/TileBuilders/CarTileBuilder.php
Normal file
67
src/Domain/Search/TileBuilders/CarTileBuilder.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilders;
|
||||
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\CarPropertyCollection;
|
||||
use App\Domain\Model\CarPropertyType;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Image;
|
||||
use App\Domain\Model\Value\Acceleration;
|
||||
use App\Domain\Model\Value\Price;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\Tiles\AccelerationTile;
|
||||
use App\Domain\Search\Tiles\CarTile;
|
||||
use App\Domain\Search\Tiles\PowerTile;
|
||||
use App\Domain\Search\Tiles\PriceTile;
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
|
||||
class CarTileBuilder
|
||||
{
|
||||
public function build(Brand $brand, CarModel $carModel, CarRevision $carRevision, CarPropertyCollection $carProperties, TileCollection $tiles): void
|
||||
{
|
||||
$subTiles = new TileCollection([]);
|
||||
|
||||
if ($carProperties->hasTypes([
|
||||
CarPropertyType::CATALOG_PRICE,
|
||||
CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL,
|
||||
CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION,
|
||||
])) {
|
||||
$imageProperty = $carProperties->getOne(CarPropertyType::IMAGE_EXTERNAL_PUBLIC_URL);
|
||||
$priceProperty = $carProperties->getOne(CarPropertyType::CATALOG_PRICE);
|
||||
$accelerationProperty = $carProperties->getOne(CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION);
|
||||
|
||||
if ($imageProperty !== null && $priceProperty !== null && $accelerationProperty !== null) {
|
||||
// Handle Image - it expects externalPublicUrl as string|null
|
||||
$imageValue = $imageProperty->value;
|
||||
$image = $imageValue instanceof Image ? $imageValue : new Image(is_string($imageValue) ? $imageValue : null);
|
||||
|
||||
// Handle Price - it should already be a Price object
|
||||
$priceValue = $priceProperty->value;
|
||||
if (!$priceValue instanceof Price) {
|
||||
return; // Skip if not a Price object
|
||||
}
|
||||
|
||||
// Handle Acceleration - it should already be an Acceleration object
|
||||
$accelerationValue = $accelerationProperty->value;
|
||||
if (!$accelerationValue instanceof Acceleration) {
|
||||
return; // Skip if not an Acceleration object
|
||||
}
|
||||
|
||||
$subTiles->add(new CarTile(
|
||||
$image,
|
||||
[
|
||||
new PriceTile($priceValue),
|
||||
new AccelerationTile($accelerationValue),
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$tiles->add(new SectionTile(
|
||||
$carRevision->name,
|
||||
$subTiles->array()
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -2,22 +2,25 @@
|
||||
|
||||
namespace App\Domain\Search;
|
||||
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
|
||||
class TileCollection
|
||||
final class TileCollection
|
||||
{
|
||||
/**
|
||||
* @param SectionTile[] $tiles
|
||||
* @param object[] $tiles
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly array $tiles,
|
||||
private array $tiles,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return SectionTile[]
|
||||
* @return object[]
|
||||
*/
|
||||
public function array(): array
|
||||
{
|
||||
return $this->tiles;
|
||||
}
|
||||
|
||||
public function add(object $tile): void
|
||||
{
|
||||
$this->tiles[] = $tile;
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
|
||||
use App\Domain\Model\DrivingCharacteristics;
|
||||
|
||||
final readonly class PerformanceOverviewTile
|
||||
{
|
||||
public function __construct(
|
||||
public DrivingCharacteristics $drivingCharacteristics,
|
||||
) {}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
|
||||
use App\Domain\Model\RangeProperties;
|
||||
|
||||
final readonly class RangeComparisonTile
|
||||
{
|
||||
public function __construct(
|
||||
public RangeProperties $rangeProperties,
|
||||
) {}
|
||||
}
|
||||
@ -3,21 +3,20 @@
|
||||
namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
||||
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
|
||||
class ModelMapper
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $row - Single brand row from database
|
||||
*/
|
||||
public function map(array $data): Brand
|
||||
public function map(array $row): Brand
|
||||
{
|
||||
$brandId = is_string($row['id'] ?? null) ? $row['id'] : throw new \InvalidArgumentException('Brand ID is required');
|
||||
|
||||
return new Brand(
|
||||
name: is_string($data['name'] ?? null) ? $data['name'] : '',
|
||||
logo: isset($data['logo']) && is_string($data['logo']) ? $data['logo'] : null,
|
||||
description: isset($data['description']) && is_string($data['description']) ? $data['description'] : null,
|
||||
foundedYear: isset($data['founded_year']) && is_numeric($data['founded_year']) ? (int) $data['founded_year'] : null,
|
||||
headquarters: isset($data['headquarters']) && is_string($data['headquarters']) ? $data['headquarters'] : null,
|
||||
website: isset($data['website']) && is_string($data['website']) ? $data['website'] : null,
|
||||
brandId: new BrandId($brandId),
|
||||
name: is_string($row['name'] ?? null) ? $row['name'] : throw new \InvalidArgumentException('Brand name is required'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,8 @@ namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
||||
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\BrandCollection;
|
||||
use App\Domain\Model\Persistence\PersistedBrand;
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
use App\Domain\Repository\BrandRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
@ -12,77 +13,67 @@ final class PostgreSQLBrandRepository implements BrandRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function findAll(): BrandCollection
|
||||
{
|
||||
$sql = 'SELECT * FROM brands ORDER BY name ASC';
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$brands = [];
|
||||
$mapper = new ModelMapper();
|
||||
|
||||
foreach ($result->fetchAllAssociative() as $brand) {
|
||||
$brands[] = $mapper->map($brand);
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$brands[] = $mapper->map($row);
|
||||
}
|
||||
|
||||
return new BrandCollection($brands);
|
||||
}
|
||||
|
||||
public function create(Brand $brand): PersistedBrand
|
||||
public function findByCarModel(CarModel $carModel): ?Brand
|
||||
{
|
||||
// Generate an ID for the brand since Brand model doesn't have one
|
||||
$brandId = uniqid('brand_', true);
|
||||
$sql = 'SELECT brands.* FROM brands
|
||||
INNER JOIN car_models ON brands.id = car_models.brand_id
|
||||
WHERE car_models.id = ?';
|
||||
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO brands (id, name, content)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
content = EXCLUDED.content
|
||||
SQL;
|
||||
$result = $this->connection->executeQuery($sql, [$carModel->carModelId->value]);
|
||||
$row = $result->fetchAssociative();
|
||||
|
||||
$content = json_encode([
|
||||
'logo' => $brand->logo,
|
||||
'description' => $brand->description,
|
||||
'founded_year' => $brand->foundedYear,
|
||||
'headquarters' => $brand->headquarters,
|
||||
'website' => $brand->website,
|
||||
]);
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$brandId,
|
||||
$brand->name,
|
||||
$content,
|
||||
]);
|
||||
|
||||
return new PersistedBrand($brandId, $brand);
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function update(PersistedBrand $persistedBrand): void
|
||||
$mapper = new ModelMapper();
|
||||
return $mapper->map($row);
|
||||
}
|
||||
|
||||
public function findById(BrandId $brandId): ?Brand
|
||||
{
|
||||
$brand = $persistedBrand->brand;
|
||||
$sql = 'SELECT * FROM brands WHERE id = ?';
|
||||
|
||||
$sql = <<<'SQL'
|
||||
UPDATE brands SET
|
||||
name = ?,
|
||||
content = ?
|
||||
WHERE id = ?
|
||||
SQL;
|
||||
$result = $this->connection->executeQuery($sql, [$brandId->value]);
|
||||
$row = $result->fetchAssociative();
|
||||
|
||||
$content = json_encode([
|
||||
'logo' => $brand->logo,
|
||||
'description' => $brand->description,
|
||||
'founded_year' => $brand->foundedYear,
|
||||
'headquarters' => $brand->headquarters,
|
||||
'website' => $brand->website,
|
||||
]);
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mapper = new ModelMapper();
|
||||
return $mapper->map($row);
|
||||
}
|
||||
|
||||
public function save(Brand $brand): void
|
||||
{
|
||||
$sql = 'INSERT INTO brands (id, name) VALUES (?, ?) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name';
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$brand->name,
|
||||
$content,
|
||||
$persistedBrand->id,
|
||||
$brand->brandId->value,
|
||||
$brand->name
|
||||
]);
|
||||
}
|
||||
|
||||
public function deleteAll(): void
|
||||
{
|
||||
$this->connection->executeStatement('DELETE FROM brands');
|
||||
}
|
||||
}
|
||||
@ -4,25 +4,23 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
||||
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\Brand;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
|
||||
class ModelMapper
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $row - Single car model row from database
|
||||
*/
|
||||
public function map(array $data): CarModel
|
||||
public function map(array $row): CarModel
|
||||
{
|
||||
$contentString = is_string($data['content'] ?? null) ? $data['content'] : '{}';
|
||||
$content = json_decode($contentString, true);
|
||||
|
||||
$brand = null;
|
||||
if (is_array($content) && !empty($content['brand']) && is_string($content['brand'])) {
|
||||
$brand = new Brand(name: $content['brand']);
|
||||
}
|
||||
$carModelId = is_string($row['id'] ?? null) ? $row['id'] : throw new \InvalidArgumentException('CarModel ID is required');
|
||||
$brandId = is_string($row['brand_id'] ?? null) ? $row['brand_id'] : throw new \InvalidArgumentException('Brand ID is required');
|
||||
|
||||
return new CarModel(
|
||||
name: is_string($data['name'] ?? null) ? $data['name'] : '',
|
||||
brand: $brand,
|
||||
carModelId: new CarModelId($carModelId),
|
||||
brandId: new BrandId($brandId),
|
||||
name: is_string($row['name'] ?? null) ? $row['name'] : throw new \InvalidArgumentException('CarModel name is required'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,9 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
||||
|
||||
use App\Domain\Model\CarModel;
|
||||
use App\Domain\Model\CarModelCollection;
|
||||
use App\Domain\Model\Persistence\PersistedCarModel;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Model\Value\BrandId;
|
||||
use App\Domain\Repository\CarModelRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
@ -12,108 +14,89 @@ final class PostgreSQLCarModelRepository implements CarModelRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function findAll(): CarModelCollection
|
||||
{
|
||||
$sql = 'SELECT * FROM car_models ORDER BY name ASC';
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$carModels = [];
|
||||
$mapper = new ModelMapper();
|
||||
|
||||
foreach ($result->fetchAllAssociative() as $carModel) {
|
||||
$carModels[] = $mapper->map($carModel);
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carModels[] = $mapper->map($row);
|
||||
}
|
||||
|
||||
return new CarModelCollection($carModels);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?PersistedCarModel
|
||||
public function findById(CarModelId $carModelId): ?CarModel
|
||||
{
|
||||
$sql = 'SELECT * FROM car_models WHERE id = ?';
|
||||
$result = $this->connection->executeQuery($sql, [$id]);
|
||||
$data = $result->fetchAssociative();
|
||||
|
||||
if (!$data) {
|
||||
$result = $this->connection->executeQuery($sql, [$carModelId->value]);
|
||||
$row = $result->fetchAssociative();
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mapper = new ModelMapper();
|
||||
$carModel = $mapper->map($data);
|
||||
|
||||
return new PersistedCarModel(is_string($data['id'] ?? null) ? $data['id'] : '', $carModel);
|
||||
return $mapper->map($row);
|
||||
}
|
||||
|
||||
public function findByBrandId(string $brandId): CarModelCollection
|
||||
public function findByBrandId(BrandId $brandId): CarModelCollection
|
||||
{
|
||||
$sql = 'SELECT * FROM car_models WHERE brand_id = ? ORDER BY name ASC';
|
||||
$result = $this->connection->executeQuery($sql, [$brandId]);
|
||||
|
||||
$result = $this->connection->executeQuery($sql, [$brandId->value]);
|
||||
$carModels = [];
|
||||
$mapper = new ModelMapper();
|
||||
|
||||
foreach ($result->fetchAllAssociative() as $carModel) {
|
||||
$carModels[] = $mapper->map($carModel);
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carModels[] = $mapper->map($row);
|
||||
}
|
||||
|
||||
return new CarModelCollection($carModels);
|
||||
}
|
||||
|
||||
public function create(CarModel $carModel, string $brandId): PersistedCarModel
|
||||
public function findByCarRevision(CarRevision $carRevision): ?CarModel
|
||||
{
|
||||
// Generate an ID for the car model
|
||||
$carModelId = uniqid('carmodel_', true);
|
||||
$sql = 'SELECT car_models.* FROM car_models
|
||||
INNER JOIN car_revisions ON car_models.id = car_revisions.car_model_id
|
||||
WHERE car_revisions.id = ?';
|
||||
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO car_models (id, brand_id, name, content)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
brand_id = EXCLUDED.brand_id,
|
||||
name = EXCLUDED.name,
|
||||
content = EXCLUDED.content
|
||||
SQL;
|
||||
$result = $this->connection->executeQuery($sql, [$carRevision->carRevisionId->value]);
|
||||
$row = $result->fetchAssociative();
|
||||
|
||||
$content = json_encode([
|
||||
'brand' => $carModel->brand->name ?? null,
|
||||
]);
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$carModelId,
|
||||
$brandId,
|
||||
$carModel->name,
|
||||
$content,
|
||||
]);
|
||||
|
||||
return new PersistedCarModel($carModelId, $carModel);
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function update(PersistedCarModel $persistedCarModel): void
|
||||
$mapper = new ModelMapper();
|
||||
return $mapper->map($row);
|
||||
}
|
||||
|
||||
public function save(CarModel $carModel): void
|
||||
{
|
||||
$carModel = $persistedCarModel->carModel;
|
||||
|
||||
$sql = <<<'SQL'
|
||||
UPDATE car_models SET
|
||||
name = ?,
|
||||
content = ?
|
||||
WHERE id = ?
|
||||
SQL;
|
||||
|
||||
$content = json_encode([
|
||||
'brand' => $carModel->brand->name ?? null,
|
||||
]);
|
||||
$sql = 'INSERT INTO car_models (id, brand_id, name) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET brand_id = EXCLUDED.brand_id, name = EXCLUDED.name';
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$carModel->carModelId->value,
|
||||
$carModel->brandId->value,
|
||||
$carModel->name,
|
||||
$content,
|
||||
$persistedCarModel->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(PersistedCarModel $persistedCarModel): void
|
||||
public function delete(CarModel $carModel): void
|
||||
{
|
||||
$sql = 'DELETE FROM car_models WHERE id = ?';
|
||||
$this->connection->executeStatement($sql, [$persistedCarModel->id]);
|
||||
$this->connection->executeStatement($sql, [$carModel->carModelId->value]);
|
||||
}
|
||||
|
||||
public function deleteAll(): void
|
||||
{
|
||||
$this->connection->executeStatement('DELETE FROM car_models');
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Infrastructure\PostgreSQL\Repository\CarPropertyRepository;
|
||||
|
||||
use App\Domain\Model\CarProperty;
|
||||
use App\Domain\Model\CarPropertyCollection;
|
||||
use App\Domain\Model\CarPropertyType;
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\Value\CarPropertyId;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Repository\CarPropertyRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
final class PostgreSQLCarPropertyRepository implements CarPropertyRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {}
|
||||
|
||||
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection
|
||||
{
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT * FROM car_properties WHERE car_revision_id = ?',
|
||||
[$carRevision->carRevisionId->value]
|
||||
);
|
||||
|
||||
$carProperties = [];
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carProperty = $this->mapRowToCarProperty($row);
|
||||
if ($carProperty !== null) {
|
||||
$carProperties[] = $carProperty;
|
||||
}
|
||||
}
|
||||
|
||||
return new CarPropertyCollection($carProperties);
|
||||
}
|
||||
|
||||
public function findByEmbeddingIds(array $embeddingIds): CarPropertyCollection
|
||||
{
|
||||
// Placeholder implementation - would need to join with embeddings table
|
||||
return new CarPropertyCollection([]);
|
||||
}
|
||||
|
||||
public function findByEmbeddingPhraseHashes(array $phraseHashes): CarPropertyCollection
|
||||
{
|
||||
if (empty($phraseHashes)) {
|
||||
return new CarPropertyCollection([]);
|
||||
}
|
||||
|
||||
$placeholders = str_repeat('?,', count($phraseHashes) - 1) . '?';
|
||||
$result = $this->connection->executeQuery(
|
||||
"SELECT cp.* FROM car_properties cp
|
||||
INNER JOIN embeddings e ON e.phrase_hash IN ($placeholders)
|
||||
WHERE cp.id = e.car_property_id",
|
||||
array_values($phraseHashes)
|
||||
);
|
||||
|
||||
$carProperties = [];
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carProperty = $this->mapRowToCarProperty($row);
|
||||
if ($carProperty !== null) {
|
||||
$carProperties[] = $carProperty;
|
||||
}
|
||||
}
|
||||
|
||||
return new CarPropertyCollection($carProperties);
|
||||
}
|
||||
|
||||
public function save(CarProperty $carProperty): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO car_properties (id, car_revision_id, type, value) VALUES (?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET car_revision_id = EXCLUDED.car_revision_id, type = EXCLUDED.type, value = EXCLUDED.value',
|
||||
[
|
||||
$carProperty->carPropertyId->value,
|
||||
$carProperty->carRevisionId->value,
|
||||
$carProperty->type->value,
|
||||
serialize($carProperty->value),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(CarProperty $carProperty): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM car_properties WHERE id = ?',
|
||||
[$carProperty->carPropertyId->value]
|
||||
);
|
||||
}
|
||||
|
||||
public function deleteAll(): void
|
||||
{
|
||||
$this->connection->executeStatement('DELETE FROM car_properties');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function mapRowToCarProperty(array $row): ?CarProperty
|
||||
{
|
||||
try {
|
||||
$id = $row['id'] ?? null;
|
||||
$carRevisionId = $row['car_revision_id'] ?? null;
|
||||
$type = $row['type'] ?? null;
|
||||
$value = $row['value'] ?? null;
|
||||
|
||||
if (!is_string($id) || !is_string($carRevisionId) || !is_string($type) || !is_string($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CarProperty(
|
||||
new CarPropertyId($id),
|
||||
new CarRevisionId($carRevisionId),
|
||||
CarPropertyType::from($type),
|
||||
unserialize($value)
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// Invalid data, skip this row
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,128 +3,23 @@
|
||||
namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
||||
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\DrivingCharacteristics;
|
||||
use App\Domain\Model\Image;
|
||||
use App\Domain\Model\Value\Date;
|
||||
use App\Domain\Model\Value\Price;
|
||||
use App\Domain\Model\Value\Currency;
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
use App\Domain\Model\Battery\CellChemistry;
|
||||
use App\Domain\Model\Charging\ChargingProperties;
|
||||
use App\Domain\Model\RangeProperties;
|
||||
use App\Domain\Model\Range\WltpRange;
|
||||
use App\Domain\Model\Range\NefzRange;
|
||||
use App\Domain\Model\Value\Energy;
|
||||
use App\Domain\Model\Value\Power;
|
||||
use App\Domain\Model\Value\Acceleration;
|
||||
use App\Domain\Model\Value\Speed;
|
||||
use App\Domain\Model\Value\Consumption;
|
||||
use App\Domain\Model\Value\Range;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
|
||||
class ModelMapper
|
||||
{
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @param array<string, mixed> $row - Single car revision row from database
|
||||
*/
|
||||
public function map(array $data): CarRevision
|
||||
public function map(array $row): CarRevision
|
||||
{
|
||||
$contentString = is_string($data['content'] ?? null) ? $data['content'] : '{}';
|
||||
$content = json_decode($contentString, true);
|
||||
|
||||
if (!is_array($content)) {
|
||||
$content = [];
|
||||
}
|
||||
|
||||
$productionBegin = null;
|
||||
if (isset($content['production_begin']) && is_numeric($content['production_begin'])) {
|
||||
$productionBegin = new Date(1, 1, (int) $content['production_begin']);
|
||||
}
|
||||
|
||||
$productionEnd = null;
|
||||
if (isset($content['production_end']) && is_numeric($content['production_end'])) {
|
||||
$productionEnd = new Date(1, 1, (int) $content['production_end']);
|
||||
}
|
||||
|
||||
$catalogPrice = null;
|
||||
if (isset($content['catalog_price']) && isset($content['catalog_price_currency']) &&
|
||||
is_numeric($content['catalog_price']) && is_string($content['catalog_price_currency'])) {
|
||||
$currency = match($content['catalog_price_currency']) {
|
||||
'EUR' => Currency::euro(),
|
||||
'USD' => Currency::usd(),
|
||||
default => Currency::euro(), // fallback to euro
|
||||
};
|
||||
|
||||
$catalogPrice = new Price(
|
||||
(int) $content['catalog_price'],
|
||||
$currency
|
||||
);
|
||||
}
|
||||
|
||||
$image = null;
|
||||
if (isset($content['image_url']) && is_string($content['image_url'])) {
|
||||
$image = new Image($content['image_url']);
|
||||
}
|
||||
|
||||
$drivingCharacteristics = null;
|
||||
if (isset($content['driving_characteristics']) && is_array($content['driving_characteristics'])) {
|
||||
$dc = $content['driving_characteristics'];
|
||||
$drivingCharacteristics = new DrivingCharacteristics(
|
||||
power: (isset($dc['power_kw']) && is_numeric($dc['power_kw'])) ? new Power((float) $dc['power_kw']) : null,
|
||||
acceleration: (isset($dc['acceleration_0_100']) && is_numeric($dc['acceleration_0_100'])) ? new Acceleration((float) $dc['acceleration_0_100']) : null,
|
||||
topSpeed: (isset($dc['top_speed_kmh']) && is_numeric($dc['top_speed_kmh'])) ? new Speed((int) $dc['top_speed_kmh']) : null,
|
||||
consumption: (isset($dc['consumption_kwh_100km']) && is_numeric($dc['consumption_kwh_100km'])) ? new Consumption(new Energy((float) $dc['consumption_kwh_100km'])) : null,
|
||||
);
|
||||
}
|
||||
|
||||
$battery = null;
|
||||
if (isset($content['battery']) && is_array($content['battery'])) {
|
||||
$b = $content['battery'];
|
||||
if (isset($b['usable_capacity_kwh']) && isset($b['total_capacity_kwh']) &&
|
||||
is_numeric($b['usable_capacity_kwh']) && is_numeric($b['total_capacity_kwh'])) {
|
||||
$battery = new BatteryProperties(
|
||||
usableCapacity: new Energy((float) $b['usable_capacity_kwh']),
|
||||
totalCapacity: new Energy((float) $b['total_capacity_kwh']),
|
||||
cellChemistry: (isset($b['cell_chemistry']) && (is_string($b['cell_chemistry']) || is_int($b['cell_chemistry']))) ? CellChemistry::from($b['cell_chemistry']) : CellChemistry::LithiumIronPhosphate,
|
||||
model: is_string($b['model'] ?? null) ? $b['model'] : '',
|
||||
manufacturer: is_string($b['manufacturer'] ?? null) ? $b['manufacturer'] : '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$chargingProperties = null;
|
||||
if (isset($content['charging']) && is_array($content['charging']) &&
|
||||
isset($content['charging']['top_charging_speed_kw']) &&
|
||||
is_numeric($content['charging']['top_charging_speed_kw'])) {
|
||||
$chargingProperties = new ChargingProperties(
|
||||
topChargingSpeed: new Power((float) $content['charging']['top_charging_speed_kw'])
|
||||
);
|
||||
}
|
||||
|
||||
$rangeProperties = null;
|
||||
if (isset($content['range']) && is_array($content['range'])) {
|
||||
$r = $content['range'];
|
||||
$wltp = (isset($r['wltp_km']) && is_numeric($r['wltp_km'])) ? new WltpRange(new Range((int) $r['wltp_km'])) : null;
|
||||
$nefz = (isset($r['nefz_km']) && is_numeric($r['nefz_km'])) ? new NefzRange(new Range((int) $r['nefz_km'])) : null;
|
||||
|
||||
if ($wltp || $nefz) {
|
||||
$rangeProperties = new RangeProperties(
|
||||
wltp: $wltp,
|
||||
nefz: $nefz,
|
||||
);
|
||||
}
|
||||
}
|
||||
$carRevisionId = is_string($row['id'] ?? null) ? $row['id'] : throw new \InvalidArgumentException('CarRevision ID is required');
|
||||
$carModelId = is_string($row['car_model_id'] ?? null) ? $row['car_model_id'] : throw new \InvalidArgumentException('CarModel ID is required');
|
||||
|
||||
return new CarRevision(
|
||||
name: is_string($data['name'] ?? null) ? $data['name'] : '',
|
||||
productionBegin: $productionBegin,
|
||||
productionEnd: $productionEnd,
|
||||
drivingCharacteristics: $drivingCharacteristics,
|
||||
battery: $battery,
|
||||
chargingProperties: $chargingProperties,
|
||||
rangeProperties: $rangeProperties,
|
||||
catalogPrice: $catalogPrice,
|
||||
carModel: null, // CarModel would need to be loaded separately if needed
|
||||
image: $image,
|
||||
carRevisionId: new CarRevisionId($carRevisionId),
|
||||
carModelId: new CarModelId($carModelId),
|
||||
name: is_string($row['name'] ?? null) ? $row['name'] : throw new \InvalidArgumentException('CarRevision name is required'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,8 @@ namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
||||
|
||||
use App\Domain\Model\CarRevision;
|
||||
use App\Domain\Model\CarRevisionCollection;
|
||||
use App\Domain\Model\Persistence\PersistedCarRevision;
|
||||
use App\Domain\Model\Value\CarRevisionId;
|
||||
use App\Domain\Model\Value\CarModelId;
|
||||
use App\Domain\Repository\CarRevisionRepository;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
@ -12,156 +13,74 @@ final class PostgreSQLCarRevisionRepository implements CarRevisionRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function findAll(): CarRevisionCollection
|
||||
{
|
||||
$sql = 'SELECT * FROM car_revisions ORDER BY name ASC';
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
|
||||
$result = $this->connection->executeQuery($sql);
|
||||
$carRevisions = [];
|
||||
$mapper = new ModelMapper();
|
||||
|
||||
foreach ($result->fetchAllAssociative() as $carRevision) {
|
||||
$carRevisions[] = $mapper->map($carRevision);
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carRevisions[] = $mapper->map($row);
|
||||
}
|
||||
|
||||
return new CarRevisionCollection($carRevisions);
|
||||
}
|
||||
|
||||
public function findById(string $id): ?PersistedCarRevision
|
||||
public function findById(CarRevisionId $carRevisionId): ?CarRevision
|
||||
{
|
||||
$sql = 'SELECT * FROM car_revisions WHERE id = ?';
|
||||
$result = $this->connection->executeQuery($sql, [$id]);
|
||||
$data = $result->fetchAssociative();
|
||||
|
||||
if (!$data) {
|
||||
$result = $this->connection->executeQuery($sql, [$carRevisionId->value]);
|
||||
$row = $result->fetchAssociative();
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mapper = new ModelMapper();
|
||||
$carRevision = $mapper->map($data);
|
||||
|
||||
return new PersistedCarRevision(is_string($data['id'] ?? null) ? $data['id'] : '', $carRevision);
|
||||
return $mapper->map($row);
|
||||
}
|
||||
|
||||
public function findByCarModelId(string $carModelId): CarRevisionCollection
|
||||
public function findByCarModelId(CarModelId $carModelId): CarRevisionCollection
|
||||
{
|
||||
$sql = 'SELECT * FROM car_revisions WHERE car_model_id = ? ORDER BY name ASC';
|
||||
$result = $this->connection->executeQuery($sql, [$carModelId]);
|
||||
|
||||
$result = $this->connection->executeQuery($sql, [$carModelId->value]);
|
||||
$carRevisions = [];
|
||||
$mapper = new ModelMapper();
|
||||
|
||||
foreach ($result->fetchAllAssociative() as $carRevision) {
|
||||
$carRevisions[] = $mapper->map($carRevision);
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$carRevisions[] = $mapper->map($row);
|
||||
}
|
||||
|
||||
return new CarRevisionCollection($carRevisions);
|
||||
}
|
||||
|
||||
public function create(CarRevision $carRevision, string $carModelId): PersistedCarRevision
|
||||
public function save(CarRevision $carRevision): void
|
||||
{
|
||||
// Generate an ID for the car revision
|
||||
$carRevisionId = uniqid('carrevision_', true);
|
||||
|
||||
$sql = <<<'SQL'
|
||||
INSERT INTO car_revisions (id, car_model_id, name, content)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
car_model_id = EXCLUDED.car_model_id,
|
||||
name = EXCLUDED.name,
|
||||
content = EXCLUDED.content
|
||||
SQL;
|
||||
|
||||
$content = json_encode([
|
||||
'production_begin' => $carRevision->productionBegin->year ?? null,
|
||||
'production_end' => $carRevision->productionEnd->year ?? null,
|
||||
'catalog_price' => $carRevision->catalogPrice->price ?? null,
|
||||
'catalog_price_currency' => $carRevision->catalogPrice?->currency->currency ?? null,
|
||||
'image_url' => $carRevision->image->externalPublicUrl ?? null,
|
||||
'driving_characteristics' => [
|
||||
'power_kw' => $carRevision->drivingCharacteristics->power->kilowatts ?? null,
|
||||
'acceleration_0_100' => $carRevision->drivingCharacteristics->acceleration->secondsFrom0To100 ?? null,
|
||||
'top_speed_kmh' => $carRevision->drivingCharacteristics->topSpeed->kmh ?? null,
|
||||
'consumption_kwh_100km' => $carRevision->drivingCharacteristics?->consumption?->energyPer100Km->kwh() ?? null,
|
||||
],
|
||||
'battery' => [
|
||||
'usable_capacity_kwh' => $carRevision->battery?->usableCapacity->kwh() ?? null,
|
||||
'total_capacity_kwh' => $carRevision->battery?->totalCapacity->kwh() ?? null,
|
||||
'cell_chemistry' => $carRevision->battery?->cellChemistry->value ?? null,
|
||||
'model' => $carRevision->battery->model ?? null,
|
||||
'manufacturer' => $carRevision->battery->manufacturer ?? null,
|
||||
],
|
||||
'charging' => [
|
||||
'top_charging_speed_kw' => $carRevision->chargingProperties->topChargingSpeed->kilowatts ?? null,
|
||||
],
|
||||
'range' => [
|
||||
'wltp_km' => $carRevision->rangeProperties?->wltp?->range->kilometers ?? null,
|
||||
'nefz_km' => $carRevision->rangeProperties?->nefz?->range->kilometers ?? null,
|
||||
],
|
||||
]);
|
||||
$sql = 'INSERT INTO car_revisions (id, car_model_id, name) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET car_model_id = EXCLUDED.car_model_id, name = EXCLUDED.name';
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$carRevisionId,
|
||||
$carModelId,
|
||||
$carRevision->name,
|
||||
$content,
|
||||
$carRevision->carRevisionId->value,
|
||||
$carRevision->carModelId->value,
|
||||
$carRevision->name
|
||||
]);
|
||||
|
||||
return new PersistedCarRevision($carRevisionId, $carRevision);
|
||||
return;
|
||||
}
|
||||
|
||||
public function update(PersistedCarRevision $persistedCarRevision): void
|
||||
{
|
||||
$carRevision = $persistedCarRevision->carRevision;
|
||||
|
||||
$sql = <<<'SQL'
|
||||
UPDATE car_revisions SET
|
||||
name = ?,
|
||||
content = ?
|
||||
WHERE id = ?
|
||||
SQL;
|
||||
|
||||
$content = json_encode([
|
||||
'production_begin' => $carRevision->productionBegin->year ?? null,
|
||||
'production_end' => $carRevision->productionEnd->year ?? null,
|
||||
'catalog_price' => $carRevision->catalogPrice->price ?? null,
|
||||
'catalog_price_currency' => $carRevision->catalogPrice?->currency->currency ?? null,
|
||||
'image_url' => $carRevision->image->externalPublicUrl ?? null,
|
||||
'driving_characteristics' => [
|
||||
'power_kw' => $carRevision->drivingCharacteristics->power->kilowatts ?? null,
|
||||
'acceleration_0_100' => $carRevision->drivingCharacteristics->acceleration->secondsFrom0To100 ?? null,
|
||||
'top_speed_kmh' => $carRevision->drivingCharacteristics->topSpeed->kmh ?? null,
|
||||
'consumption_kwh_100km' => $carRevision->drivingCharacteristics?->consumption?->energyPer100Km->kwh() ?? null,
|
||||
],
|
||||
'battery' => [
|
||||
'usable_capacity_kwh' => $carRevision->battery?->usableCapacity->kwh() ?? null,
|
||||
'total_capacity_kwh' => $carRevision->battery?->totalCapacity->kwh() ?? null,
|
||||
'cell_chemistry' => $carRevision->battery?->cellChemistry->value ?? null,
|
||||
'model' => $carRevision->battery->model ?? null,
|
||||
'manufacturer' => $carRevision->battery->manufacturer ?? null,
|
||||
],
|
||||
'charging' => [
|
||||
'top_charging_speed_kw' => $carRevision->chargingProperties->topChargingSpeed->kilowatts ?? null,
|
||||
],
|
||||
'range' => [
|
||||
'wltp_km' => $carRevision->rangeProperties?->wltp?->range->kilometers ?? null,
|
||||
'nefz_km' => $carRevision->rangeProperties?->nefz?->range->kilometers ?? null,
|
||||
],
|
||||
]);
|
||||
|
||||
$this->connection->executeStatement($sql, [
|
||||
$carRevision->name,
|
||||
$content,
|
||||
$persistedCarRevision->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(PersistedCarRevision $persistedCarRevision): void
|
||||
public function delete(CarRevision $carRevision): void
|
||||
{
|
||||
$sql = 'DELETE FROM car_revisions WHERE id = ?';
|
||||
$this->connection->executeStatement($sql, [$persistedCarRevision->id]);
|
||||
$this->connection->executeStatement($sql, [$carRevision->carRevisionId->value]);
|
||||
}
|
||||
|
||||
public function deleteAll(): void
|
||||
{
|
||||
$this->connection->executeStatement('DELETE FROM car_revisions');
|
||||
}
|
||||
}
|
||||
@ -7,84 +7,148 @@ use App\Domain\Repository\EmbeddingRepository;
|
||||
use App\Domain\Model\AI\Embedding;
|
||||
use App\Domain\Model\AI\LargeEmbeddingVector;
|
||||
use App\Domain\Model\AI\SmallEmbeddingVector;
|
||||
use App\Domain\Model\Persistence\PersistedEmbedding;
|
||||
use App\Domain\Model\EmbeddingCollection;
|
||||
use App\Domain\Model\Value\Vector;
|
||||
use App\Domain\Model\Value\EmbeddingId;
|
||||
|
||||
class PostgreSQLEmbeddingRepository implements EmbeddingRepository
|
||||
final class PostgreSQLEmbeddingRepository implements EmbeddingRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {
|
||||
}
|
||||
) {}
|
||||
|
||||
public function create(Embedding $embedding): PersistedEmbedding
|
||||
public function save(Embedding $embedding): void
|
||||
{
|
||||
$hash = md5($embedding->phrase);
|
||||
$this->connection->executeStatement(<<<SQL
|
||||
INSERT INTO embeddings (phrase_hash, phrase, large_embedding_vector, small_embedding_vector)
|
||||
VALUES (:phrase_hash, :phrase, :large_embedding_vector, :small_embedding_vector)
|
||||
SQL, [
|
||||
'phrase_hash' => $hash,
|
||||
'phrase' => $embedding->phrase,
|
||||
'large_embedding_vector' => $embedding->largeEmbeddingVector !== null ? '[' . implode(',', $embedding->largeEmbeddingVector->vector->values) . ']' : null,
|
||||
'small_embedding_vector' => $embedding->smallEmbeddingVector !== null ? '[' . implode(',', $embedding->smallEmbeddingVector->vector->values) . ']' : null,
|
||||
]);
|
||||
|
||||
return new PersistedEmbedding(
|
||||
$hash,
|
||||
$embedding,
|
||||
$this->connection->executeStatement(
|
||||
'INSERT INTO embeddings (id, phrase_hash, phrase, large_embedding_vector, small_embedding_vector) VALUES (?, ?, ?, ?, ?) ON CONFLICT DO NOTHING',
|
||||
[
|
||||
$embedding->embeddingId->value,
|
||||
$embedding->phraseHash(),
|
||||
$embedding->phrase,
|
||||
$embedding->largeEmbeddingVector !== null
|
||||
? '[' . implode(',', $embedding->largeEmbeddingVector->vector->values) . ']'
|
||||
: null,
|
||||
$embedding->smallEmbeddingVector !== null
|
||||
? '[' . implode(',', $embedding->smallEmbeddingVector->vector->values) . ']'
|
||||
: null,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function delete(PersistedEmbedding $persistedEmbedding): void
|
||||
public function delete(Embedding $embedding): void
|
||||
{
|
||||
$this->connection->delete('embeddings', ['phrase_hash' => $persistedEmbedding->phraseHash]);
|
||||
$this->connection->executeStatement(
|
||||
'DELETE FROM embeddings WHERE id = ?',
|
||||
[$embedding->embeddingId->value]
|
||||
);
|
||||
}
|
||||
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): array
|
||||
public function findByPhrase(string $phrase): ?Embedding
|
||||
{
|
||||
$result = $this->connection->executeQuery(<<<SQL
|
||||
SELECT phrase_hash, phrase, large_embedding_vector, small_embedding_vector, created_at
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT * FROM embeddings WHERE phrase = ?',
|
||||
[$phrase]
|
||||
);
|
||||
|
||||
$row = $result->fetchAssociative();
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mapRowToEmbedding($row);
|
||||
}
|
||||
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): EmbeddingCollection
|
||||
{
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT *
|
||||
FROM embeddings
|
||||
WHERE large_embedding_vector IS NOT NULL
|
||||
ORDER BY large_embedding_vector <=> :vector
|
||||
LIMIT :limit
|
||||
SQL, [
|
||||
'vector' => '[' . implode(',', $embeddingVector->vector->values) . ']',
|
||||
'limit' => $limit,
|
||||
]);
|
||||
ORDER BY large_embedding_vector <=> ?
|
||||
LIMIT ?',
|
||||
[
|
||||
'[' . implode(',', $embeddingVector->vector->values) . ']',
|
||||
$limit,
|
||||
]
|
||||
);
|
||||
|
||||
$embeddings = [];
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$embeddings[] = new PersistedEmbedding(
|
||||
is_string($row['phrase_hash'] ?? null) ? $row['phrase_hash'] : '',
|
||||
new Embedding(is_string($row['phrase'] ?? null) ? $row['phrase'] : '', $embeddingVector)
|
||||
);
|
||||
$embeddings[] = $this->mapRowToEmbedding($row);
|
||||
}
|
||||
|
||||
return $embeddings;
|
||||
return new EmbeddingCollection($embeddings);
|
||||
}
|
||||
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 10): array
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 10): EmbeddingCollection
|
||||
{
|
||||
$result = $this->connection->executeQuery(<<<SQL
|
||||
SELECT phrase_hash, phrase, large_embedding_vector, small_embedding_vector, created_at
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT *
|
||||
FROM embeddings
|
||||
WHERE small_embedding_vector IS NOT NULL
|
||||
ORDER BY small_embedding_vector <=> :vector
|
||||
LIMIT :limit
|
||||
SQL, [
|
||||
'vector' => '[' . implode(',', $smallEmbeddingVector->vector->values) . ']',
|
||||
'limit' => $limit,
|
||||
]);
|
||||
ORDER BY small_embedding_vector <=> ?
|
||||
LIMIT ?',
|
||||
[
|
||||
'[' . implode(',', $smallEmbeddingVector->vector->values) . ']',
|
||||
$limit,
|
||||
]
|
||||
);
|
||||
|
||||
$embeddings = [];
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
$embeddings[] = new PersistedEmbedding(
|
||||
is_string($row['phrase_hash'] ?? null) ? $row['phrase_hash'] : '',
|
||||
new Embedding(is_string($row['phrase'] ?? null) ? $row['phrase'] : '', null, $smallEmbeddingVector)
|
||||
);
|
||||
$embeddings[] = $this->mapRowToEmbedding($row);
|
||||
}
|
||||
|
||||
return $embeddings;
|
||||
return new EmbeddingCollection($embeddings);
|
||||
}
|
||||
|
||||
public function deleteAll(): void
|
||||
{
|
||||
$this->connection->executeStatement('DELETE FROM embeddings');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function mapRowToEmbedding(array $row): Embedding
|
||||
{
|
||||
$largeEmbeddingVector = null;
|
||||
if (is_string($row['large_embedding_vector'] ?? null)) {
|
||||
$largeValues = json_decode($row['large_embedding_vector'], true);
|
||||
if (is_array($largeValues)) {
|
||||
$floatValues = [];
|
||||
foreach ($largeValues as $value) {
|
||||
if (is_numeric($value)) {
|
||||
$floatValues[] = (float) $value;
|
||||
}
|
||||
}
|
||||
if (count($floatValues) === count($largeValues)) {
|
||||
$largeEmbeddingVector = new LargeEmbeddingVector(new Vector($floatValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$smallEmbeddingVector = null;
|
||||
if (is_string($row['small_embedding_vector'] ?? null)) {
|
||||
$smallValues = json_decode($row['small_embedding_vector'], true);
|
||||
if (is_array($smallValues)) {
|
||||
$floatValues = [];
|
||||
foreach ($smallValues as $value) {
|
||||
if (is_numeric($value)) {
|
||||
$floatValues[] = (float) $value;
|
||||
}
|
||||
}
|
||||
if (count($floatValues) === count($smallValues)) {
|
||||
$smallEmbeddingVector = new SmallEmbeddingVector(new Vector($floatValues));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Embedding(
|
||||
embeddingId: new EmbeddingId(is_string($row['id'] ?? null) ? $row['id'] : throw new \InvalidArgumentException('Embedding ID is required')),
|
||||
phrase: is_string($row['phrase'] ?? null) ? $row['phrase'] : throw new \InvalidArgumentException('Phrase is required'),
|
||||
largeEmbeddingVector: $largeEmbeddingVector,
|
||||
smallEmbeddingVector: $smallEmbeddingVector,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@
|
||||
<h2 class="section-title"><i class="fas fa-industry"></i> Popular Electric Vehicle Brands</h2>
|
||||
<div class="brands-grid">
|
||||
{% for brand in brands %}
|
||||
<div class="brand-card" data-brand-id="{{ brand.id }}">
|
||||
<div class="brand-card">
|
||||
<div class="brand-name"><i class="fas fa-car"></i> {{ brand.name }}</div>
|
||||
{% if brand.description %}
|
||||
<div class="brand-description">{{ brand.description }}</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user