first attempt to use an ai based search
This commit is contained in:
parent
acd669e180
commit
3f78e2e9f1
@ -30,7 +30,9 @@
|
|||||||
"phpstan/phpstan-symfony": "^2.0",
|
"phpstan/phpstan-symfony": "^2.0",
|
||||||
"symfony/debug-bundle": "7.2.*",
|
"symfony/debug-bundle": "7.2.*",
|
||||||
"symfony/maker-bundle": "^1.0",
|
"symfony/maker-bundle": "^1.0",
|
||||||
"symfony/var-dumper": "7.2.*"
|
"symfony/stopwatch": "^7.3",
|
||||||
|
"symfony/var-dumper": "7.2.*",
|
||||||
|
"symfony/web-profiler-bundle": "^7.3"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
|
|||||||
87
composer.lock
generated
87
composer.lock
generated
@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "aff00a8d8bd55186f046d02ac6fd7c4d",
|
"content-hash": "e4da23c3811aae55314b0e018026605a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/semver",
|
"name": "composer/semver",
|
||||||
@ -4572,6 +4572,91 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-05-15T09:04:05+00:00"
|
"time": "2025-05-15T09:04:05+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/web-profiler-bundle",
|
||||||
|
"version": "v7.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/web-profiler-bundle.git",
|
||||||
|
"reference": "a22b7e4a744820a56f1bafa830f2c72a2ba0913c"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/web-profiler-bundle/zipball/a22b7e4a744820a56f1bafa830f2c72a2ba0913c",
|
||||||
|
"reference": "a22b7e4a744820a56f1bafa830f2c72a2ba0913c",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"composer-runtime-api": ">=2.1",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/config": "^7.3",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/framework-bundle": "^6.4|^7.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0",
|
||||||
|
"symfony/routing": "^6.4|^7.0",
|
||||||
|
"symfony/twig-bundle": "^6.4|^7.0",
|
||||||
|
"twig/twig": "^3.12"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/form": "<6.4",
|
||||||
|
"symfony/mailer": "<6.4",
|
||||||
|
"symfony/messenger": "<6.4",
|
||||||
|
"symfony/serializer": "<7.2",
|
||||||
|
"symfony/workflow": "<7.3"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"symfony/browser-kit": "^6.4|^7.0",
|
||||||
|
"symfony/console": "^6.4|^7.0",
|
||||||
|
"symfony/css-selector": "^6.4|^7.0",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Bundle\\WebProfilerBundle\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides a development tool that gives detailed information about the execution of any request",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/web-profiler-bundle/tree/v7.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2025-05-02T05:30:54+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/yaml",
|
"name": "symfony/yaml",
|
||||||
"version": "v7.2.6",
|
"version": "v7.2.6",
|
||||||
|
|||||||
@ -7,4 +7,5 @@ return [
|
|||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
11
config/packages/web_profiler.yaml
Normal file
11
config/packages/web_profiler.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
when@dev:
|
||||||
|
web_profiler:
|
||||||
|
toolbar: true
|
||||||
|
|
||||||
|
framework:
|
||||||
|
profiler:
|
||||||
|
collect_serializer_data: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
profiler: { collect: false }
|
||||||
8
config/routes/web_profiler.yaml
Normal file
8
config/routes/web_profiler.yaml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
when@dev:
|
||||||
|
web_profiler_wdt:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
|
||||||
|
prefix: /_wdt
|
||||||
|
|
||||||
|
web_profiler_profiler:
|
||||||
|
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
|
||||||
|
prefix: /_profiler
|
||||||
@ -15,3 +15,11 @@ services:
|
|||||||
App\Domain\AI\AIClient:
|
App\Domain\AI\AIClient:
|
||||||
arguments:
|
arguments:
|
||||||
$apiKey: '%env(OPENAI_API_KEY)%'
|
$apiKey: '%env(OPENAI_API_KEY)%'
|
||||||
|
|
||||||
|
App\Application\DataCollector\AiChatDataCollector:
|
||||||
|
arguments:
|
||||||
|
$aiClient: '@App\Domain\AI\AIClient'
|
||||||
|
tags:
|
||||||
|
- { name: data_collector, id: 'ai_chat', template: 'profiler/ai-chat.html.twig' }
|
||||||
|
|
||||||
|
App\Domain\Search\Engine: '@App\Domain\Search\AiTileEngine'
|
||||||
29
migrations/Version20250609005825.php
Normal file
29
migrations/Version20250609005825.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?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 Version20250609005825 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Remove car property type column';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE car_properties DROP COLUMN type');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE car_properties ADD COLUMN type VARCHAR(255) NOT NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,9 +3,9 @@
|
|||||||
namespace App\Application\Commands;
|
namespace App\Application\Commands;
|
||||||
|
|
||||||
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Image;
|
use App\Domain\Model\Image;
|
||||||
use App\Domain\Model\Value\Date;
|
use App\Domain\Model\Value\Date;
|
||||||
use App\Domain\Model\Value\Price;
|
use App\Domain\Model\Value\Price;
|
||||||
@ -14,12 +14,23 @@ use App\Domain\Model\Id\BrandId;
|
|||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Model\Id\CarPropertyId;
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
use App\Domain\Model\Battery\CellChemistry;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\CarProperty;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\CarPropertyType;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||||
use App\Domain\Model\Value\Energy;
|
use App\Domain\Model\Value\Energy;
|
||||||
use App\Domain\Model\Value\Power;
|
use App\Domain\Model\Value\Power;
|
||||||
use App\Domain\Model\Value\Acceleration;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\AverageConsumption;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryCapacity;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryType;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\CellChemistry;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CatalogPrice;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargingSpeed;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\MotorPower;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
||||||
|
use App\Domain\Model\Range\NefzRange;
|
||||||
|
use App\Domain\Model\Range\WltpRange;
|
||||||
use App\Domain\Model\Value\Speed;
|
use App\Domain\Model\Value\Speed;
|
||||||
use App\Domain\Model\Value\Consumption;
|
use App\Domain\Model\Value\Consumption;
|
||||||
use App\Domain\Model\Value\Range;
|
use App\Domain\Model\Value\Range;
|
||||||
@ -145,15 +156,14 @@ class LoadFixtures extends Command
|
|||||||
|
|
||||||
$this->carRevisionRepository->save($carRevision);
|
$this->carRevisionRepository->save($carRevision);
|
||||||
|
|
||||||
foreach ($revisionFixture['properties'] as $propertyData) {
|
foreach ($revisionFixture['properties'] as $propertyValue) {
|
||||||
$property = new CarProperty(
|
$property = new CarProperty(
|
||||||
CarPropertyId::generate(),
|
CarPropertyId::generate(),
|
||||||
$carRevision->carRevisionId,
|
$carRevision->carRevisionId,
|
||||||
$propertyData['type'],
|
$propertyValue
|
||||||
$propertyData['value']
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->carPropertyEmbedder->createPersistedEmbedding($property, $carRevision, $brand);
|
$this->carPropertyEmbedder->createPersistedEmbedding($property, $carRevision, $carModel, $brand);
|
||||||
$this->carPropertyRepository->save($property);
|
$this->carPropertyRepository->save($property);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,11 +180,183 @@ class LoadFixtures extends Command
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<array{brand: string, models: array<array{model: string, revisions: array<array{revision: string, image: Image, properties: array<array{type: CarPropertyType, value: mixed}>}>}>}>
|
* @return array<array{brand: string, models: array<array{model: string, revisions: array<array{revision: string, image: Image|null, properties: array<CarPropertyValue>}>}>}>
|
||||||
*/
|
*/
|
||||||
private function getFixtures(): array
|
private function getFixtures(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
[
|
||||||
|
'brand' => 'Skoda',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'Elroq',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '85',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(10, 1, 2024)),
|
||||||
|
new MotorPower(new Power(210)), // Estimated from 0-100 time and specs
|
||||||
|
new Acceleration(6.6),
|
||||||
|
new TopSpeed(new Speed(180)), // Estimated typical for this class
|
||||||
|
new AverageConsumption(new Consumption(new Energy(17.1))),
|
||||||
|
new BatteryCapacity(new Energy(77.0), new Energy(82.0)), // Usable/gross capacity
|
||||||
|
new BatteryType(CellChemistry::LithiumIronPhosphate, 'MEB Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(120), new Power(120)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(493)), new WltpRange(new Range(450))),
|
||||||
|
new CatalogPrice(new Price(43900, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Mercedes-Benz',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'CLA',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '250+',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(5, 1, 2025)),
|
||||||
|
new MotorPower(new Power(200)), // Estimated from acceleration
|
||||||
|
new Acceleration(6.7),
|
||||||
|
new TopSpeed(new Speed(210)), // Estimated
|
||||||
|
new AverageConsumption(new Consumption(new Energy(15.0))),
|
||||||
|
new BatteryCapacity(new Energy(85.0), new Energy(90.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MMA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(170), new Power(170)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(700)), new WltpRange(new Range(565))),
|
||||||
|
new CatalogPrice(new Price(55859, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => 'EQS',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '450+',
|
||||||
|
'image' => new Image(externalPublicUrl: 'https://ev-database.org/img/auto/Mercedes_EQS_2024/Mercedes_EQS_2024-01@2x.jpg'),
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(1, 1, 2021)),
|
||||||
|
new MotorPower(new Power(245)),
|
||||||
|
new Acceleration(6.2),
|
||||||
|
new TopSpeed(new Speed(210)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(15.7))),
|
||||||
|
new BatteryCapacity(new Energy(90.0), new Energy(107.8)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'EVA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(200), new Power(200)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(770)), new WltpRange(new Range(756))),
|
||||||
|
new CatalogPrice(new Price(106374, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Kia',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'EV3',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '81.4 kWh',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(8, 1, 2024)),
|
||||||
|
new MotorPower(new Power(150)), // Estimated from acceleration
|
||||||
|
new Acceleration(7.7),
|
||||||
|
new TopSpeed(new Speed(170)), // Estimated
|
||||||
|
new AverageConsumption(new Consumption(new Energy(17.1))),
|
||||||
|
new BatteryCapacity(new Energy(78.0), new Energy(81.4)),
|
||||||
|
new BatteryType(CellChemistry::LithiumIronPhosphate, 'E-GMP Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(105), new Power(105)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(472)), new WltpRange(new Range(455))),
|
||||||
|
new CatalogPrice(new Price(41390, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Smart',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => '#5',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Premium',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(5, 1, 2025)),
|
||||||
|
new MotorPower(new Power(250)), // Estimated from acceleration
|
||||||
|
new Acceleration(6.5),
|
||||||
|
new TopSpeed(new Speed(180)), // Estimated
|
||||||
|
new AverageConsumption(new Consumption(new Energy(20.2))),
|
||||||
|
new BatteryCapacity(new Energy(94.0), new Energy(100.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'SEA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(230), new Power(230)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(579)), new WltpRange(new Range(465))),
|
||||||
|
new CatalogPrice(new Price(55400, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Volkswagen',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'ID.7',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Pro',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(8, 1, 2023)),
|
||||||
|
new MotorPower(new Power(210)), // Estimated from acceleration
|
||||||
|
new Acceleration(6.5),
|
||||||
|
new TopSpeed(new Speed(180)), // Estimated
|
||||||
|
new AverageConsumption(new Consumption(new Energy(16.2))),
|
||||||
|
new BatteryCapacity(new Energy(77.0), new Energy(82.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MEB Platform', 'LG Energy Solution'),
|
||||||
|
new ChargingSpeed(new Power(170), new Power(170)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(621)), new WltpRange(new Range(475))),
|
||||||
|
new CatalogPrice(new Price(53995, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => 'ID.4',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Pro',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(1, 1, 2020)),
|
||||||
|
new MotorPower(new Power(150)),
|
||||||
|
new Acceleration(8.5),
|
||||||
|
new TopSpeed(new Speed(160)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(16.3))),
|
||||||
|
new BatteryCapacity(new Energy(77.0), new Energy(82.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'MEB Platform', 'LG Energy Solution'),
|
||||||
|
new ChargingSpeed(new Power(125), new Power(125)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(549)), new WltpRange(new Range(520))),
|
||||||
|
new CatalogPrice(new Price(51515, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'brand' => 'Tesla',
|
'brand' => 'Tesla',
|
||||||
'models' => [
|
'models' => [
|
||||||
@ -185,20 +367,16 @@ class LoadFixtures extends Command
|
|||||||
'revision' => 'Plaid',
|
'revision' => 'Plaid',
|
||||||
'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-S-Main-Hero-Desktop-LHD.jpg'),
|
'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-S-Main-Hero-Desktop-LHD.jpg'),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
new Production(productionBegin: new Date(1, 1, 2021)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(750)],
|
new MotorPower(new Power(750)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.1)],
|
new Acceleration(2.1),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(322)],
|
new TopSpeed(new Speed(322)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.3))],
|
new AverageConsumption(new Consumption(new Energy(19.3))),
|
||||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(95.0)],
|
new BatteryCapacity(new Energy(95.0), new Energy(100.0)),
|
||||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(100.0)],
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, '4680', 'Tesla'),
|
||||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
new ChargingSpeed(new Power(250), new Power(250)),
|
||||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => '4680'],
|
new RangeSpecification(new NefzRange(new Range(652)), new WltpRange(new Range(628))),
|
||||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'],
|
new CatalogPrice(new Price(129990, Currency::euro())),
|
||||||
['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())],
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -210,20 +388,16 @@ class LoadFixtures extends Command
|
|||||||
'revision' => 'Long Range',
|
'revision' => 'Long Range',
|
||||||
'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg'),
|
'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg'),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2020)],
|
new Production(productionBegin: new Date(1, 1, 2020)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(366)],
|
new MotorPower(new Power(366)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.4)],
|
new Acceleration(4.4),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(233)],
|
new TopSpeed(new Speed(233)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(14.9))],
|
new AverageConsumption(new Consumption(new Energy(14.9))),
|
||||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(75.0)],
|
new BatteryCapacity(new Energy(75.0), new Energy(82.0)),
|
||||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(82.0)],
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, '2170', 'Tesla'),
|
||||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
new ChargingSpeed(new Power(250), new Power(250)),
|
||||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => '2170'],
|
new RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(602))),
|
||||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'Tesla'],
|
new CatalogPrice(new Price(129990, Currency::euro())),
|
||||||
['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())],
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -238,22 +412,18 @@ class LoadFixtures extends Command
|
|||||||
'revisions' => [
|
'revisions' => [
|
||||||
[
|
[
|
||||||
'revision' => 'xDrive50',
|
'revision' => 'xDrive50',
|
||||||
'image' => new Image(externalPublicUrl: 'https://www.bmw.de/content/dam/bmw/common/all-models/x-series/ix/2021/highlights/bmw-ix-sp-desktop.jpg'),
|
'image' => new Image(externalPublicUrl: 'https://cdn.motor1.com/images/mgl/N7Lgn/s1/bmw-ix-xdrive50-2021.jpg'),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
new Production(productionBegin: new Date(1, 1, 2021)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(385)],
|
new MotorPower(new Power(385)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(4.6)],
|
new Acceleration(4.6),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(200)],
|
new TopSpeed(new Speed(200)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.8))],
|
new AverageConsumption(new Consumption(new Energy(19.8))),
|
||||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(71.2)],
|
new BatteryCapacity(new Energy(71.2), new Energy(76.6)),
|
||||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(76.6)],
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'BMW Gen5', 'CATL'),
|
||||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
new ChargingSpeed(new Power(195), new Power(195)),
|
||||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'BMW Gen5'],
|
new RangeSpecification(new NefzRange(new Range(680)), new WltpRange(new Range(630))),
|
||||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'CATL'],
|
new CatalogPrice(new Price(77300, Currency::euro())),
|
||||||
['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())],
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -268,82 +438,18 @@ class LoadFixtures extends Command
|
|||||||
'revisions' => [
|
'revisions' => [
|
||||||
[
|
[
|
||||||
'revision' => 'RS',
|
'revision' => 'RS',
|
||||||
'image' => 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'),
|
'image' => new Image(externalPublicUrl: 'https://ev-database.org/img/auto/Audi_e-tron_GT_RS/Audi_e-tron_GT_RS-02@2x.jpg'),
|
||||||
'properties' => [
|
'properties' => [
|
||||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2021)],
|
new Production(productionBegin: new Date(1, 1, 2021)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(475)],
|
new MotorPower(new Power(475)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(3.3)],
|
new Acceleration(3.3),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(250)],
|
new TopSpeed(new Speed(250)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(19.6))],
|
new AverageConsumption(new Consumption(new Energy(19.6))),
|
||||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)],
|
new BatteryCapacity(new Energy(83.7), new Energy(93.4)),
|
||||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)],
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'PPE Platform', 'LG Energy Solution'),
|
||||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
new ChargingSpeed(new Power(270), new Power(270)),
|
||||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'PPE Platform'],
|
new RangeSpecification(new NefzRange(new Range(487)), new WltpRange(new Range(472))),
|
||||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'],
|
new CatalogPrice(new Price(142900, Currency::euro())),
|
||||||
['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())],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'brand' => 'Mercedes-Benz',
|
|
||||||
'models' => [
|
|
||||||
[
|
|
||||||
'model' => 'EQS',
|
|
||||||
'revisions' => [
|
|
||||||
[
|
|
||||||
'revision' => '450+',
|
|
||||||
'image' => 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'),
|
|
||||||
'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())],
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'brand' => 'Volkswagen',
|
|
||||||
'models' => [
|
|
||||||
[
|
|
||||||
'model' => 'ID.4',
|
|
||||||
'revisions' => [
|
|
||||||
[
|
|
||||||
'revision' => 'Pro',
|
|
||||||
'image' => 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'),
|
|
||||||
'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())],
|
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@ -358,22 +464,310 @@ class LoadFixtures extends Command
|
|||||||
'revisions' => [
|
'revisions' => [
|
||||||
[
|
[
|
||||||
'revision' => 'Turbo S',
|
'revision' => 'Turbo S',
|
||||||
'image' => 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'),
|
'image' => null,
|
||||||
'properties' => [
|
'properties' => [
|
||||||
['type' => CarPropertyType::PRODUCTION_BEGIN, 'value' => new Date(1, 1, 2019)],
|
new Production(productionBegin: new Date(1, 1, 2019)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_POWER, 'value' => new Power(560)],
|
new MotorPower(new Power(560)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION, 'value' => new Acceleration(2.8)],
|
new Acceleration(2.8),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_TOP_SPEED, 'value' => new Speed(260)],
|
new TopSpeed(new Speed(260)),
|
||||||
['type' => CarPropertyType::DRIVING_CHARACTERISTICS_CONSUMPTION, 'value' => new Consumption(new Energy(23.7))],
|
new AverageConsumption(new Consumption(new Energy(23.7))),
|
||||||
['type' => CarPropertyType::BATTERY_USABLE_CAPACITY, 'value' => new Energy(83.7)],
|
new BatteryCapacity(new Energy(83.7), new Energy(93.4)),
|
||||||
['type' => CarPropertyType::BATTERY_TOTAL_CAPACITY, 'value' => new Energy(93.4)],
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'J1 Platform', 'LG Energy Solution'),
|
||||||
['type' => CarPropertyType::BATTERY_CELL_CHEMISTRY, 'value' => CellChemistry::LithiumNickelManganeseOxide],
|
new ChargingSpeed(new Power(270), new Power(270)),
|
||||||
['type' => CarPropertyType::BATTERY_MODEL, 'value' => 'J1 Platform'],
|
new RangeSpecification(new NefzRange(new Range(452)), new WltpRange(new Range(440))),
|
||||||
['type' => CarPropertyType::BATTERY_MANUFACTURER, 'value' => 'LG Energy Solution'],
|
new CatalogPrice(new Price(185456, Currency::euro())),
|
||||||
['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())],
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Ford',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'Mustang Mach-E',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Extended Range RWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(1, 1, 2021)),
|
||||||
|
new MotorPower(new Power(269)),
|
||||||
|
new Acceleration(7.0),
|
||||||
|
new TopSpeed(new Speed(180)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(18.7))),
|
||||||
|
new BatteryCapacity(new Energy(88.0), new Energy(98.8)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'GE2 Platform', 'LG Energy Solution'),
|
||||||
|
new ChargingSpeed(new Power(150), new Power(150)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(610)), new WltpRange(new Range(600))),
|
||||||
|
new CatalogPrice(new Price(72900, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Hyundai',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'IONIQ 6',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '77.4 kWh RWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(7, 1, 2022)),
|
||||||
|
new MotorPower(new Power(229)),
|
||||||
|
new Acceleration(7.4),
|
||||||
|
new TopSpeed(new Speed(185)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(14.3))),
|
||||||
|
new BatteryCapacity(new Energy(77.4), new Energy(84.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'),
|
||||||
|
new ChargingSpeed(new Power(233), new Power(233)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(614))),
|
||||||
|
new CatalogPrice(new Price(53900, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => 'IONIQ 5',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '77.4 kWh AWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(2, 1, 2021)),
|
||||||
|
new MotorPower(new Power(239)),
|
||||||
|
new Acceleration(5.2),
|
||||||
|
new TopSpeed(new Speed(185)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(17.7))),
|
||||||
|
new BatteryCapacity(new Energy(77.4), new Energy(84.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'),
|
||||||
|
new ChargingSpeed(new Power(233), new Power(233)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(507)), new WltpRange(new Range(507))),
|
||||||
|
new CatalogPrice(new Price(59900, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Polestar',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => '2',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Long Range Single Motor',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(1, 1, 2020)),
|
||||||
|
new MotorPower(new Power(170)),
|
||||||
|
new Acceleration(7.4),
|
||||||
|
new TopSpeed(new Speed(160)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(16.5))),
|
||||||
|
new BatteryCapacity(new Energy(78.0), new Energy(82.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(150), new Power(150)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(540)), new WltpRange(new Range(635))),
|
||||||
|
new CatalogPrice(new Price(51400, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => '3',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Long Range RWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(10, 1, 2023)),
|
||||||
|
new MotorPower(new Power(220)),
|
||||||
|
new Acceleration(7.0),
|
||||||
|
new TopSpeed(new Speed(180)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(15.8))),
|
||||||
|
new BatteryCapacity(new Energy(78.0), new Energy(84.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'SPA2 Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(250), new Power(250)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(610)), new WltpRange(new Range(610))),
|
||||||
|
new CatalogPrice(new Price(73400, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Volvo',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'EX30',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Extended Range Single Motor',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(6, 1, 2023)),
|
||||||
|
new MotorPower(new Power(200)),
|
||||||
|
new Acceleration(5.3),
|
||||||
|
new TopSpeed(new Speed(180)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(14.8))),
|
||||||
|
new BatteryCapacity(new Energy(69.0), new Energy(72.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumIronPhosphate, 'SEA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(153), new Power(153)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(476)), new WltpRange(new Range(476))),
|
||||||
|
new CatalogPrice(new Price(41700, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => 'XC40',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Extended Range',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(10, 1, 2021)),
|
||||||
|
new MotorPower(new Power(170)),
|
||||||
|
new Acceleration(7.3),
|
||||||
|
new TopSpeed(new Speed(160)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(18.8))),
|
||||||
|
new BatteryCapacity(new Energy(78.0), new Energy(82.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMA Platform', 'CATL'),
|
||||||
|
new ChargingSpeed(new Power(150), new Power(150)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(425)), new WltpRange(new Range(425))),
|
||||||
|
new CatalogPrice(new Price(52950, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Genesis',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'GV70',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Electrified AWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(6, 1, 2022)),
|
||||||
|
new MotorPower(new Power(360)),
|
||||||
|
new Acceleration(4.2),
|
||||||
|
new TopSpeed(new Speed(225)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(20.9))),
|
||||||
|
new BatteryCapacity(new Energy(77.4), new Energy(84.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'E-GMP Platform', 'SK Innovation'),
|
||||||
|
new ChargingSpeed(new Power(233), new Power(233)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(455)), new WltpRange(new Range(455))),
|
||||||
|
new CatalogPrice(new Price(74800, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Lucid',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'Air',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => 'Dream Edition Range',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(10, 1, 2021)),
|
||||||
|
new MotorPower(new Power(696)),
|
||||||
|
new Acceleration(2.5),
|
||||||
|
new TopSpeed(new Speed(270)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(13.8))),
|
||||||
|
new BatteryCapacity(new Energy(113.0), new Energy(118.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'LCAP Platform', 'Samsung SDI'),
|
||||||
|
new ChargingSpeed(new Power(300), new Power(300)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(883)), new WltpRange(new Range(883))),
|
||||||
|
new CatalogPrice(new Price(218000, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'Nissan',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'Ariya',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '87 kWh FWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(7, 1, 2022)),
|
||||||
|
new MotorPower(new Power(178)),
|
||||||
|
new Acceleration(7.5),
|
||||||
|
new TopSpeed(new Speed(160)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(17.6))),
|
||||||
|
new BatteryCapacity(new Energy(87.0), new Energy(91.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, 'CMF-EV Platform', 'Envision AESC'),
|
||||||
|
new ChargingSpeed(new Power(130), new Power(130)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(533)), new WltpRange(new Range(533))),
|
||||||
|
new CatalogPrice(new Price(59990, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'brand' => 'BYD',
|
||||||
|
'models' => [
|
||||||
|
[
|
||||||
|
'model' => 'Tang',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '86.4 kWh AWD',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(1, 1, 2023)),
|
||||||
|
new MotorPower(new Power(380)),
|
||||||
|
new Acceleration(4.6),
|
||||||
|
new TopSpeed(new Speed(180)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(17.9))),
|
||||||
|
new BatteryCapacity(new Energy(86.4), new Energy(89.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumIronPhosphate, 'e-Platform 3.0', 'BYD'),
|
||||||
|
new ChargingSpeed(new Power(170), new Power(170)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(530)), new WltpRange(new Range(530))),
|
||||||
|
new CatalogPrice(new Price(72990, Currency::euro())),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'model' => 'Atto 3',
|
||||||
|
'revisions' => [
|
||||||
|
[
|
||||||
|
'revision' => '60.48 kWh',
|
||||||
|
'image' => null,
|
||||||
|
'properties' => [
|
||||||
|
new Production(productionBegin: new Date(9, 1, 2022)),
|
||||||
|
new MotorPower(new Power(150)),
|
||||||
|
new Acceleration(7.3),
|
||||||
|
new TopSpeed(new Speed(160)),
|
||||||
|
new AverageConsumption(new Consumption(new Energy(15.4))),
|
||||||
|
new BatteryCapacity(new Energy(60.48), new Energy(64.0)),
|
||||||
|
new BatteryType(CellChemistry::LithiumIronPhosphate, 'e-Platform 3.0', 'BYD'),
|
||||||
|
new ChargingSpeed(new Power(88), new Power(88)),
|
||||||
|
new RangeSpecification(new NefzRange(new Range(420)), new WltpRange(new Range(420))),
|
||||||
|
new CatalogPrice(new Price(44490, Currency::euro())),
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
39
src/Application/DataCollector/AiChatDataCollector.php
Normal file
39
src/Application/DataCollector/AiChatDataCollector.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Application\DataCollector;
|
||||||
|
|
||||||
|
use App\Domain\AI\AIClient;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||||
|
|
||||||
|
class AiChatDataCollector extends DataCollector
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AIClient $aiClient,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
|
||||||
|
{
|
||||||
|
$this->data = $this->aiClient->getLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return 'ai_chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<mixed>
|
||||||
|
*/
|
||||||
|
public function getLog(): array
|
||||||
|
{
|
||||||
|
return $this->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reset(): void
|
||||||
|
{
|
||||||
|
$this->data = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,6 +11,11 @@ class AIClient
|
|||||||
{
|
{
|
||||||
private readonly OpenAI\Client $client;
|
private readonly OpenAI\Client $client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<array{prompt: string, response: string}>
|
||||||
|
*/
|
||||||
|
private array $log = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $apiKey,
|
private readonly string $apiKey,
|
||||||
) {
|
) {
|
||||||
@ -20,15 +25,48 @@ class AIClient
|
|||||||
public function generateText(string $prompt): string
|
public function generateText(string $prompt): string
|
||||||
{
|
{
|
||||||
$response = $this->client->chat()->create([
|
$response = $this->client->chat()->create([
|
||||||
'model' => 'gpt-4o-mini',
|
'model' => 'gpt-4.1-nano-2025-04-14',
|
||||||
'messages' => [
|
'messages' => [
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->log[] = [
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'response' => $response->choices[0]->message->content ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
return $response->choices[0]->message->content ?? '';
|
return $response->choices[0]->message->content ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<mixed>
|
||||||
|
*/
|
||||||
|
public function generateJson(string $prompt): array
|
||||||
|
{
|
||||||
|
$response = $this->client->chat()->create([
|
||||||
|
'model' => 'gpt-4.1-nano',
|
||||||
|
'messages' => [
|
||||||
|
['role' => 'user', 'content' => $prompt],
|
||||||
|
],
|
||||||
|
'response_format' => [
|
||||||
|
'type' => 'json_object',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->log[] = [
|
||||||
|
'prompt' => $prompt,
|
||||||
|
'response' => $response->choices[0]->message->content ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$result = json_decode($response->choices[0]->message->content ?? '', true) ?? [];
|
||||||
|
if (!is_array($result)) {
|
||||||
|
throw new \Exception('Invalid JSON response from AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
public function embedTextLarge(string $text): LargeEmbeddingVector
|
public function embedTextLarge(string $text): LargeEmbeddingVector
|
||||||
{
|
{
|
||||||
$response = $this->client->embeddings()->create([
|
$response = $this->client->embeddings()->create([
|
||||||
@ -48,4 +86,17 @@ class AIClient
|
|||||||
|
|
||||||
return new SmallEmbeddingVector(new Vector($response->embeddings[0]->embedding));
|
return new SmallEmbeddingVector(new Vector($response->embeddings[0]->embedding));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<array{prompt: string, response: string}>
|
||||||
|
*/
|
||||||
|
public function getLog(): array
|
||||||
|
{
|
||||||
|
return $this->log;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetLog(): void
|
||||||
|
{
|
||||||
|
$this->log = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -4,12 +4,13 @@ namespace App\Domain\ContentManagement;
|
|||||||
|
|
||||||
use App\Domain\AI\AIClient;
|
use App\Domain\AI\AIClient;
|
||||||
use App\Domain\Model\Embedding\Embedding;
|
use App\Domain\Model\Embedding\Embedding;
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\CarProperty;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\EmbeddingId;
|
use App\Domain\Model\Id\EmbeddingId;
|
||||||
use App\Domain\Repository\EmbeddingRepository;
|
use App\Domain\Repository\EmbeddingRepository;
|
||||||
use Stringable;
|
|
||||||
|
|
||||||
class CarPropertyEmbedder
|
class CarPropertyEmbedder
|
||||||
{
|
{
|
||||||
@ -19,16 +20,17 @@ class CarPropertyEmbedder
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?Brand $brand): ?Embedding
|
public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?CarModel $carModel, ?Brand $brand): ?Embedding
|
||||||
{
|
{
|
||||||
if (!($carProperty->value instanceof Stringable)) {
|
$text = $carProperty->value->humanReadable();
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$text = $carProperty->type->humanReadable() . ': ' . (string) $carProperty->value;
|
|
||||||
if ($carRevision !== null) {
|
if ($carRevision !== null) {
|
||||||
$text .= ' - ' . $carRevision->name;
|
$text .= ' - ' . $carRevision->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($carModel !== null) {
|
||||||
|
$text .= ', ' . $carModel->name;
|
||||||
|
}
|
||||||
|
|
||||||
if ($brand !== null) {
|
if ($brand !== null) {
|
||||||
$text .= ', ' . $brand->name;
|
$text .= ', ' . $brand->name;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Battery;
|
|
||||||
|
|
||||||
use App\Domain\Model\Value\Energy;
|
|
||||||
|
|
||||||
class BatteryProperties
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly Energy $usableCapacity,
|
|
||||||
public readonly Energy $totalCapacity,
|
|
||||||
public readonly CellChemistry $cellChemistry = CellChemistry::LithiumIronPhosphate,
|
|
||||||
public readonly string $model = '4680',
|
|
||||||
public readonly string $manufacturer = 'Tesla',
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->usableCapacity->__toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
<?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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
<?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';
|
|
||||||
|
|
||||||
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::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',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
|
use App\Domain\Model\EmbeddingCollection;
|
||||||
|
|
||||||
final readonly class Brand
|
final readonly class Brand
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
class BrandCollection
|
class BrandCollection
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
class CarModelCollection
|
class CarModelCollection
|
||||||
{
|
{
|
||||||
@ -1,18 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\Id\CarPropertyId;
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of CarPropertyValue
|
||||||
|
*/
|
||||||
final readonly class CarProperty
|
final readonly class CarProperty
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param T $value
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly CarPropertyId $carPropertyId,
|
public readonly CarPropertyId $carPropertyId,
|
||||||
public readonly CarRevisionId $carRevisionId,
|
public readonly CarRevisionId $carRevisionId,
|
||||||
public CarPropertyType $type,
|
public CarPropertyValue $value,
|
||||||
public mixed $value,
|
|
||||||
public readonly EmbeddingIdCollection $embeddings = new EmbeddingIdCollection([]),
|
public readonly EmbeddingIdCollection $embeddings = new EmbeddingIdCollection([]),
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
107
src/Domain/Model/Cars/CarPropertyCollection.php
Normal file
107
src/Domain/Model/Cars/CarPropertyCollection.php
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template TItem of CarPropertyValue
|
||||||
|
*/
|
||||||
|
final class CarPropertyCollection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param CarProperty<TItem>[] $properties
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private array $properties = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param CarProperty<TItem> $property
|
||||||
|
*/
|
||||||
|
public function add(CarProperty $property): void
|
||||||
|
{
|
||||||
|
$this->properties[] = $property;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function count(): int
|
||||||
|
{
|
||||||
|
return count($this->properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return CarProperty<TItem>[]
|
||||||
|
*/
|
||||||
|
public function array(): array
|
||||||
|
{
|
||||||
|
return $this->properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return CarPropertyCollection<TItem>
|
||||||
|
*/
|
||||||
|
public function sortByRevision(): CarPropertyCollection
|
||||||
|
{
|
||||||
|
$properties = $this->properties;
|
||||||
|
usort($properties, fn(CarProperty $a, CarProperty $b) => $a->carRevisionId->value <=> $b->carRevisionId->value);
|
||||||
|
return new CarPropertyCollection($properties);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of CarPropertyValue
|
||||||
|
*
|
||||||
|
* @param class-string<T> $type
|
||||||
|
*
|
||||||
|
* @return CarPropertyCollection<T>
|
||||||
|
*/
|
||||||
|
public function get(string $type): CarPropertyCollection
|
||||||
|
{
|
||||||
|
/** @var CarPropertyCollection<T> $collection */
|
||||||
|
$collection = new CarPropertyCollection(array_filter($this->properties, fn(CarProperty $property) => $property->value instanceof $type));
|
||||||
|
return $collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of CarPropertyValue
|
||||||
|
*
|
||||||
|
* @param class-string<T> $type
|
||||||
|
*
|
||||||
|
* @return CarProperty<T>|null
|
||||||
|
*/
|
||||||
|
public function getOne(string $type): ?CarProperty
|
||||||
|
{
|
||||||
|
return array_values($this->get($type)->array())[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of CarPropertyValue
|
||||||
|
*
|
||||||
|
* @param class-string<T> $type
|
||||||
|
*
|
||||||
|
* @return T|null
|
||||||
|
*/
|
||||||
|
public function getOneValue(string $type): mixed
|
||||||
|
{
|
||||||
|
$value = $this->getOne($type)?->value;
|
||||||
|
if ($value === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string<TItem>[] $types
|
||||||
|
*/
|
||||||
|
public function hasTypes(array $types): bool
|
||||||
|
{
|
||||||
|
foreach ($types as $type) {
|
||||||
|
if ($this->get($type)->count() === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php
Normal file
21
src/Domain/Model/Cars/CarPropertyValues/V1/Acceleration.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
final readonly class Acceleration extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly float $secondsFrom0To100,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Acceleration: ' . $this->secondsFrom0To100 . ' sec (0-100 km/h)';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function seconds(): float
|
||||||
|
{
|
||||||
|
return $this->secondsFrom0To100;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Consumption;
|
||||||
|
|
||||||
|
final readonly class AverageConsumption extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Consumption $consumption,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Average consumption: ' . $this->consumption->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1\Battery;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Energy;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
final readonly class BatteryCapacity extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Energy $usableCapacity,
|
||||||
|
public readonly Energy $totalCapacity,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return "Usable capacity: " . $this->usableCapacity->__toString() . ", total capacity: " . $this->totalCapacity->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1\Battery;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
final readonly class BatteryType extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly CellChemistry $chemistry,
|
||||||
|
public readonly string $model,
|
||||||
|
public readonly string $manufacturer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Battery type: ' . $this->chemistry->value . ' ' . $this->model . ' ' . $this->manufacturer;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model\Battery;
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1\Battery;
|
||||||
|
|
||||||
use Stringable;
|
|
||||||
|
|
||||||
enum CellChemistry: string
|
enum CellChemistry: string
|
||||||
{
|
{
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
abstract readonly class CarPropertyValue implements Stringable
|
||||||
|
{
|
||||||
|
abstract public function humanReadable(): string;
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->humanReadable();
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php
Normal file
17
src/Domain/Model/Cars/CarPropertyValues/V1/CatalogPrice.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Price;
|
||||||
|
|
||||||
|
final readonly class CatalogPrice extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Price $price,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Catalog price: ' . $this->price->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Domain\Model\Charging;
|
namespace App\Domain\Model\Charging;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\Value\Power;
|
use App\Domain\Model\Value\Power;
|
||||||
|
|
||||||
final readonly class ChargeCurve
|
final readonly class ChargeCurve extends CarPropertyValue
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public ?Power $averagePowerSoc0 = null,
|
public ?Power $averagePowerSoc0 = null,
|
||||||
@ -19,4 +20,9 @@ final readonly class ChargeCurve
|
|||||||
public ?Power $averagePowerSoc90 = null,
|
public ?Power $averagePowerSoc90 = null,
|
||||||
public ?Power $averagePowerSoc100 = null,
|
public ?Power $averagePowerSoc100 = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Charge curve: ' . $this->averagePowerSoc0 . ' ' . $this->averagePowerSoc10 . ' ' . $this->averagePowerSoc20 . ' ' . $this->averagePowerSoc30 . ' ' . $this->averagePowerSoc40 . ' ' . $this->averagePowerSoc50 . ' ' . $this->averagePowerSoc60 . ' ' . $this->averagePowerSoc70 . ' ' . $this->averagePowerSoc80 . ' ' . $this->averagePowerSoc90 . ' ' . $this->averagePowerSoc100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Charging;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
final readonly class ChargeTimeProperties extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?int $minutesFrom0To100 = null,
|
||||||
|
public ?int $minutesFrom0To70 = null,
|
||||||
|
public ?int $minutesFrom10To70 = null,
|
||||||
|
public ?int $minutesFrom20To70 = null,
|
||||||
|
public ?int $minutesFrom10To80 = null,
|
||||||
|
public ?int $minutesFrom20To80 = null,
|
||||||
|
public ?int $minutesFrom10To90 = null,
|
||||||
|
public ?int $minutesFrom20To90 = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
$properties = [];
|
||||||
|
if ($this->minutesFrom0To100 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom0To100 . ' min (0-100%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom0To70 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom0To70 . ' min (0-70%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom10To70 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom10To70 . ' min (10-70%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom20To70 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom20To70 . ' min (20-70%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom10To80 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom10To80 . ' min (10-80%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom20To80 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom20To80 . ' min (20-80%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom10To90 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom10To90 . ' min (10-90%)';
|
||||||
|
}
|
||||||
|
if ($this->minutesFrom20To90 !== null) {
|
||||||
|
$properties[] = $this->minutesFrom20To90 . ' min (20-90%)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Charge time properties: ' . implode(', ', $properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Charging;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
final readonly class ChargingConnectivity extends CarPropertyValue
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ConnectorType[] $connectorTypes
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly ?bool $is400v = null,
|
||||||
|
public readonly ?bool $is800v = null,
|
||||||
|
public readonly ?bool $plugAndCharge = null,
|
||||||
|
public readonly array $connectorTypes = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
$properties = [];
|
||||||
|
if ($this->is400v !== null) {
|
||||||
|
$properties[] = '400V';
|
||||||
|
}
|
||||||
|
if ($this->is800v !== null) {
|
||||||
|
$properties[] = '800V';
|
||||||
|
}
|
||||||
|
if ($this->plugAndCharge !== null) {
|
||||||
|
$properties[] = 'Plug and Charge: ' . ($this->plugAndCharge ? 'Yes' : 'No');
|
||||||
|
}
|
||||||
|
foreach ($this->connectorTypes as $connectorType) {
|
||||||
|
$properties[] = $connectorType->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Charging connectivity: ' . implode(', ', $properties);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1\Charging;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Model\Value\Power;
|
||||||
|
|
||||||
|
final readonly class ChargingSpeed extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Power $dcMaxKw,
|
||||||
|
public readonly Power $acMaxKw,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Charging speed: DC: ' . $this->dcMax() . ' AC: ' . $this->acMax();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dcMax(): Power
|
||||||
|
{
|
||||||
|
return $this->dcMaxKw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function acMax(): Power
|
||||||
|
{
|
||||||
|
return $this->acMaxKw;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php
Normal file
17
src/Domain/Model/Cars/CarPropertyValues/V1/MotorPower.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Power;
|
||||||
|
|
||||||
|
final readonly class MotorPower extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Power $power,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Motor power: ' . $this->power->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/Domain/Model/Cars/CarPropertyValues/V1/Production.php
Normal file
30
src/Domain/Model/Cars/CarPropertyValues/V1/Production.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Date;
|
||||||
|
|
||||||
|
final readonly class Production extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public ?Date $productionBegin = null,
|
||||||
|
public ?Date $productionEnd = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
if ($this->productionBegin === null && $this->productionEnd === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->productionBegin === null) {
|
||||||
|
return 'Production end: ' . $this->productionEnd?->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->productionEnd === null) {
|
||||||
|
return 'Production begin: ' . $this->productionBegin->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Production begin: ' . $this->productionBegin->__toString() . ' - End: ' . $this->productionEnd->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,4 +9,9 @@ final readonly class NefzRange
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly Range $range,
|
public readonly Range $range,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->range->__toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -9,4 +9,9 @@ final readonly class WltpRange
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly Range $range,
|
public readonly Range $range,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return $this->range->__toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Range\NefzRange;
|
||||||
|
use App\Domain\Model\Range\WltpRange;
|
||||||
|
|
||||||
|
final readonly class RangeSpecification extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly ?NefzRange $nefzRange = null,
|
||||||
|
public readonly ?WltpRange $wltpRange = null,
|
||||||
|
) {
|
||||||
|
if ($this->nefzRange === null && $this->wltpRange === null) {
|
||||||
|
throw new \InvalidArgumentException('At least one range must be specified');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
if ($this->nefzRange === null && $this->wltpRange === null) {
|
||||||
|
return 'No Range specified';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->nefzRange === null) {
|
||||||
|
return 'WLTP Range: ' . $this->wltpRange?->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->wltpRange === null) {
|
||||||
|
return 'NEFZ Range: ' . $this->nefzRange->__toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php
Normal file
17
src/Domain/Model/Cars/CarPropertyValues/V1/TopSpeed.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Model\Cars\CarPropertyValues\V1;
|
||||||
|
|
||||||
|
use App\Domain\Model\Value\Speed;
|
||||||
|
|
||||||
|
final readonly class TopSpeed extends CarPropertyValue
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly Speed $speed,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function humanReadable(): string
|
||||||
|
{
|
||||||
|
return 'Top speed: ' . $this->speed->__toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
class CarRevisionCollection
|
class CarRevisionCollection
|
||||||
{
|
{
|
||||||
@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Charging;
|
|
||||||
|
|
||||||
final readonly class ChargeTimeProperties
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ?int $minutesFrom0To100 = null,
|
|
||||||
public ?int $minutesFrom0To70 = null,
|
|
||||||
public ?int $minutesFrom10To70 = null,
|
|
||||||
public ?int $minutesFrom20To70 = null,
|
|
||||||
public ?int $minutesFrom10To80 = null,
|
|
||||||
public ?int $minutesFrom20To80 = null,
|
|
||||||
public ?int $minutesFrom10To90 = null,
|
|
||||||
public ?int $minutesFrom20To90 = null,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Charging;
|
|
||||||
|
|
||||||
final readonly class ChargingConnectivity
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param ConnectorType[] $connectorTypes
|
|
||||||
*/
|
|
||||||
public function __construct(
|
|
||||||
public readonly ?bool $is400v = null,
|
|
||||||
public readonly ?bool $is800v = null,
|
|
||||||
public readonly ?bool $plugAndCharge = null,
|
|
||||||
public readonly array $connectorTypes = [],
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Charging;
|
|
||||||
|
|
||||||
use App\Domain\Model\Value\Power;
|
|
||||||
|
|
||||||
final readonly class ChargingProperties
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public ?Power $topChargingSpeed = null,
|
|
||||||
public ?ChargeCurve $chargeCurve = null,
|
|
||||||
public ?ChargeTimeProperties $chargeTimeProperties = null,
|
|
||||||
public ?ChargingConnectivity $chargingConnectivity = null,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Value;
|
|
||||||
|
|
||||||
class Acceleration
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly float $secondsFrom0To100,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->secondsFrom0To100 . ' sec (0-100 km/h)';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function seconds(): float
|
|
||||||
{
|
|
||||||
return $this->secondsFrom0To100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Model\Value;
|
|
||||||
|
|
||||||
class ChargingSpeed
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
public readonly Power $dcMaxKw,
|
|
||||||
public readonly Power $acMaxKw,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function __toString(): string
|
|
||||||
{
|
|
||||||
return $this->dcMax() . ' DC / ' . $this->acMax() . ' AC';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dcMax(): Power
|
|
||||||
{
|
|
||||||
return $this->dcMaxKw;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function acMax(): Power
|
|
||||||
{
|
|
||||||
return $this->acMaxKw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domain\Repository;
|
namespace App\Domain\Repository;
|
||||||
|
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\BrandCollection;
|
use App\Domain\Model\Cars\BrandCollection;
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
|
|
||||||
interface BrandRepository
|
interface BrandRepository
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domain\Repository;
|
namespace App\Domain\Repository;
|
||||||
|
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\CarModelCollection;
|
use App\Domain\Model\Cars\CarModelCollection;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
|
|
||||||
|
|||||||
@ -2,14 +2,16 @@
|
|||||||
|
|
||||||
namespace App\Domain\Repository;
|
namespace App\Domain\Repository;
|
||||||
|
|
||||||
use App\Domain\Model\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\CarPropertyCollection;
|
use App\Domain\Model\Cars\CarPropertyCollection;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
use App\Domain\Model\Id\EmbeddingId;
|
use App\Domain\Model\Id\EmbeddingId;
|
||||||
|
|
||||||
interface CarPropertyRepository
|
interface CarPropertyRepository
|
||||||
{
|
{
|
||||||
|
public function findById(CarPropertyId $carPropertyId): ?CarProperty;
|
||||||
|
|
||||||
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection;
|
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Domain\Repository;
|
namespace App\Domain\Repository;
|
||||||
|
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\CarRevisionCollection;
|
use App\Domain\Model\Cars\CarRevisionCollection;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
|
|
||||||
|
|||||||
@ -19,14 +19,14 @@ interface EmbeddingRepository
|
|||||||
* @param int $limit
|
* @param int $limit
|
||||||
* @return EmbeddingCollection
|
* @return EmbeddingCollection
|
||||||
*/
|
*/
|
||||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 10): EmbeddingCollection;
|
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 100): EmbeddingCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param SmallEmbeddingVector $vector
|
* @param SmallEmbeddingVector $vector
|
||||||
* @param int $limit
|
* @param int $limit
|
||||||
* @return EmbeddingCollection
|
* @return EmbeddingCollection
|
||||||
*/
|
*/
|
||||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 10): EmbeddingCollection;
|
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $vector, int $limit = 100): EmbeddingCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $phrase
|
* @param string $phrase
|
||||||
|
|||||||
83
src/Domain/Repository/Loader/FullCarLoader.php
Normal file
83
src/Domain/Repository/Loader/FullCarLoader.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Repository\Loader;
|
||||||
|
|
||||||
|
use App\Domain\Model\Id\CarModelId;
|
||||||
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
|
use App\Domain\Repository\BrandRepository;
|
||||||
|
use App\Domain\Repository\CarModelRepository;
|
||||||
|
use App\Domain\Repository\CarPropertyRepository;
|
||||||
|
use App\Domain\Repository\CarRevisionRepository;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
|
|
||||||
|
final readonly class FullCarLoader
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly CarRevisionRepository $carRevisionRepository,
|
||||||
|
private readonly CarModelRepository $carModelRepository,
|
||||||
|
private readonly CarPropertyRepository $carPropertyRepository,
|
||||||
|
private readonly BrandRepository $brandRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function loadRevisionByCarPropertyId(CarPropertyId $carPropertyId): FullCarRevision
|
||||||
|
{
|
||||||
|
$carProperty = $this->carPropertyRepository->findById($carPropertyId);
|
||||||
|
if ($carProperty === null) {
|
||||||
|
throw new \InvalidArgumentException('Car property not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->loadRevision($carProperty->carRevisionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadRevision(CarRevisionId $carRevisionId): FullCarRevision
|
||||||
|
{
|
||||||
|
$carRevision = $this->carRevisionRepository->findById($carRevisionId);
|
||||||
|
if ($carRevision === null) {
|
||||||
|
throw new \InvalidArgumentException('Car revision not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$carModel = $this->carModelRepository->findById($carRevision->carModelId);
|
||||||
|
if ($carModel === null) {
|
||||||
|
throw new \InvalidArgumentException('Car model not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$brand = $this->brandRepository->findById($carModel->brandId);
|
||||||
|
if ($brand === null) {
|
||||||
|
throw new \InvalidArgumentException('Brand not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$carProperties = $this->carPropertyRepository->findByCarRevision($carRevision);
|
||||||
|
|
||||||
|
return new FullCarRevision(
|
||||||
|
$carRevision,
|
||||||
|
$carModel,
|
||||||
|
$brand,
|
||||||
|
$carProperties,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadModel(CarModelId $carModelId): FullCarModel
|
||||||
|
{
|
||||||
|
$carModel = $this->carModelRepository->findById($carModelId);
|
||||||
|
if ($carModel === null) {
|
||||||
|
throw new \InvalidArgumentException('Car model not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$brand = $this->brandRepository->findById($carModel->brandId);
|
||||||
|
if ($brand === null) {
|
||||||
|
throw new \InvalidArgumentException('Brand not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$carRevisions = $this->carRevisionRepository->findByCarModelId($carModelId);
|
||||||
|
|
||||||
|
/** @var FullCarRevision[] $fullCarRevisions */
|
||||||
|
$fullCarRevisions = array_map(fn(CarRevision $rev) => $this->loadRevision($rev->carRevisionId), $carRevisions->array());
|
||||||
|
|
||||||
|
return new FullCarModel(
|
||||||
|
$carModel,
|
||||||
|
$brand,
|
||||||
|
$fullCarRevisions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/Domain/Repository/Loader/FullCarModel.php
Normal file
40
src/Domain/Repository/Loader/FullCarModel.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Repository\Loader;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyCollection;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
|
|
||||||
|
final readonly class FullCarModel
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param FullCarRevision[] $carRevisions
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly CarModel $carModel,
|
||||||
|
private readonly Brand $brand,
|
||||||
|
private readonly array $carRevisions = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getCarModel(): CarModel
|
||||||
|
{
|
||||||
|
return $this->carModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBrand(): Brand
|
||||||
|
{
|
||||||
|
return $this->brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return FullCarRevision[]
|
||||||
|
*/
|
||||||
|
public function getCarRevisions(): array
|
||||||
|
{
|
||||||
|
return $this->carRevisions;
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/Domain/Repository/Loader/FullCarRevision.php
Normal file
45
src/Domain/Repository/Loader/FullCarRevision.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Repository\Loader;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyCollection;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
|
||||||
|
final readonly class FullCarRevision
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param CarPropertyCollection<CarPropertyValue> $carPropertyCollection
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly CarRevision $carRevision,
|
||||||
|
private readonly CarModel $carModel,
|
||||||
|
private readonly Brand $brand,
|
||||||
|
private readonly CarPropertyCollection $carPropertyCollection,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getCarRevision(): CarRevision
|
||||||
|
{
|
||||||
|
return $this->carRevision;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCarModel(): CarModel
|
||||||
|
{
|
||||||
|
return $this->carModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBrand(): Brand
|
||||||
|
{
|
||||||
|
return $this->brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return CarPropertyCollection<CarPropertyValue>
|
||||||
|
*/
|
||||||
|
public function getCarPropertyCollection(): CarPropertyCollection
|
||||||
|
{
|
||||||
|
return $this->carPropertyCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/Domain/Search/AiTileEngine.php
Normal file
121
src/Domain/Search/AiTileEngine.php
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search;
|
||||||
|
|
||||||
|
use App\Domain\AI\AIClient;
|
||||||
|
use App\Domain\Model\Embedding\Embedding;
|
||||||
|
use App\Domain\Repository\BrandRepository;
|
||||||
|
use App\Domain\Repository\CarModelRepository;
|
||||||
|
use App\Domain\Repository\CarPropertyRepository;
|
||||||
|
use App\Domain\Repository\CarRevisionRepository;
|
||||||
|
use App\Domain\Repository\EmbeddingRepository;
|
||||||
|
use App\Domain\Search\TileBuilder\AccelerationTileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\TileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\BatteryTileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\BrandTileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\PriceTileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\SubSectionTileBuilder;
|
||||||
|
use App\Domain\Search\TileBuilder\SectionTileBuilder;
|
||||||
|
use App\Domain\Search\View\ViewProvider;
|
||||||
|
|
||||||
|
final class AiTileEngine implements Engine
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EmbeddingRepository $embeddingRepository,
|
||||||
|
private readonly CarPropertyRepository $carPropertyRepository,
|
||||||
|
private readonly CarModelRepository $carModelRepository,
|
||||||
|
private readonly CarRevisionRepository $carRevisionRepository,
|
||||||
|
private readonly BrandRepository $brandRepository,
|
||||||
|
private readonly AIClient $aiClient,
|
||||||
|
private readonly ViewProvider $viewProvider,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function search(string $query): TileCollection
|
||||||
|
{
|
||||||
|
$results = $this->embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($query));
|
||||||
|
$carProperties = $this->carPropertyRepository->findByEmbeddingPhraseHashes(array_map(fn (Embedding $embedding) => $embedding->phraseHash(), $results->array()));
|
||||||
|
$carProperties = $carProperties->sortByRevision();
|
||||||
|
|
||||||
|
$contextString = "";
|
||||||
|
$currentRevisionId = null;
|
||||||
|
foreach ($carProperties->array() as $carProperty) {
|
||||||
|
$carRevision = $this->carRevisionRepository->findById($carProperty->carRevisionId);
|
||||||
|
if ($carRevision === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carModel = $this->carModelRepository->findByCarRevision($carRevision);
|
||||||
|
if ($carModel === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$brand = $this->brandRepository->findByCarModel($carModel);
|
||||||
|
if ($brand === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($currentRevisionId !== $carProperty->carRevisionId->value) {
|
||||||
|
$currentRevisionId = $carProperty->carRevisionId->value;
|
||||||
|
$contextString .= "--------------------------------\n";
|
||||||
|
$contextString .= "Model: " . $carModel->name . " (CarModelId: " . $carModel->carModelId->value . ")\n";
|
||||||
|
$contextString .= "Brand: " . $brand->name . " (BrandId: " . $brand->brandId->value . ")\n";
|
||||||
|
$contextString .= "Revision: " . $carRevision->name . " (CarRevisionId: " . $carRevision->carRevisionId->value . ")\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$contextString .= $carProperty->value->humanReadable() . " (CarPropertyId: " . $carProperty->carPropertyId->value . ")\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$views = $this->viewProvider->getAllViews();
|
||||||
|
$viewString = "";
|
||||||
|
foreach ($views as $view) {
|
||||||
|
$reflectionClass = new \ReflectionClass($view);
|
||||||
|
$shortClassName = $reflectionClass->getShortName();
|
||||||
|
|
||||||
|
$viewString .= "--------------------------------\n";
|
||||||
|
$viewString .= "View: " . $shortClassName . "\n";
|
||||||
|
$viewString .= "Description: " . $view->description() . "\n";
|
||||||
|
$viewString .= "Data description: " . json_encode($view->dataDescription()) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$input = <<<EOF
|
||||||
|
You are a powerful search engine that can answer questions about electric cars.
|
||||||
|
You are given a list of car properties that could be relevant to the question:
|
||||||
|
$contextString
|
||||||
|
|
||||||
|
The search query is:
|
||||||
|
```
|
||||||
|
$query
|
||||||
|
```
|
||||||
|
|
||||||
|
You have to decide which view to use and which data to return.
|
||||||
|
You can choose from the following views:
|
||||||
|
$viewString
|
||||||
|
|
||||||
|
View example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"view": "ExampleView",
|
||||||
|
"data": {
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
EOF;
|
||||||
|
|
||||||
|
$response = $this->aiClient->generateJson($input);
|
||||||
|
if (!isset($response['view']) || !is_string($response['view'])) {
|
||||||
|
throw new \Exception('Invalid JSON response from AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
$view = $this->viewProvider->getView($response['view']);
|
||||||
|
$data = $response['data'] ?? [];
|
||||||
|
|
||||||
|
if (!is_array($data)) {
|
||||||
|
throw new \Exception('Invalid JSON response from AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $view->build($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,27 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Domain\Search;
|
namespace App\Domain\Search;
|
||||||
|
|
||||||
use App\Domain\AI\AIClient;
|
use App\Domain\Search\TileCollection;
|
||||||
use App\Domain\Model\Embedding\Embedding;
|
|
||||||
use App\Domain\Repository\CarPropertyRepository;
|
|
||||||
use App\Domain\Repository\EmbeddingRepository;
|
|
||||||
|
|
||||||
class Engine
|
interface Engine
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function search(string $query): TileCollection;
|
||||||
private readonly EmbeddingRepository $embeddingRepository,
|
|
||||||
private readonly CarPropertyRepository $carPropertyRepository,
|
|
||||||
private readonly AIClient $aiClient,
|
|
||||||
private readonly TileBuilder $tileBuilder,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public function search(string $query): TileCollection
|
|
||||||
{
|
|
||||||
$results = $this->embeddingRepository->searchByLargeEmbeddingVector($this->aiClient->embedTextLarge($query));
|
|
||||||
|
|
||||||
$carProperties = $this->carPropertyRepository->findByEmbeddingPhraseHashes(array_map(fn (Embedding $embedding) => $embedding->phraseHash(), $results->array()));
|
|
||||||
|
|
||||||
return $this->tileBuilder->build($carProperties);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,58 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Domain\Search;
|
|
||||||
|
|
||||||
use App\Domain\Model\CarPropertyCollection;
|
|
||||||
use App\Domain\Model\Id\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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
23
src/Domain/Search/TileBuilder/AccelerationTileBuilder.php
Normal file
23
src/Domain/Search/TileBuilder/AccelerationTileBuilder.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\AccelerationTile;
|
||||||
|
|
||||||
|
final readonly class AccelerationTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if (!$carProperty->value instanceof Acceleration) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileCollection([new AccelerationTile($carProperty->value)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php
Normal file
26
src/Domain/Search/TileBuilder/AvailabilityTileBuilder.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\AvailabilityTile;
|
||||||
|
|
||||||
|
final readonly class AvailabilityTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if ($carProperty->value instanceof Production) {
|
||||||
|
return new TileCollection([new AvailabilityTile(
|
||||||
|
$carProperty->value->productionBegin,
|
||||||
|
$carProperty->value->productionEnd,
|
||||||
|
)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/Domain/Search/TileBuilder/BatteryTileBuilder.php
Normal file
28
src/Domain/Search/TileBuilder/BatteryTileBuilder.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Battery\BatteryProperties;
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryType;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\BatteryTile;
|
||||||
|
|
||||||
|
final readonly class BatteryTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if ($carProperty->value instanceof BatteryType) {
|
||||||
|
return new TileCollection([new BatteryTile(new BatteryProperties(
|
||||||
|
$carProperty->value->chemistry,
|
||||||
|
$carProperty->value->model,
|
||||||
|
$carProperty->value->manufacturer,
|
||||||
|
))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Domain/Search/TileBuilder/ChargingTileBuilder.php
Normal file
23
src/Domain/Search/TileBuilder/ChargingTileBuilder.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargeTimeProperties;
|
||||||
|
use App\Domain\Search\Tiles\ChargingTile;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
|
final readonly class ChargingTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if ($carProperty->value instanceof ChargingProperties) {
|
||||||
|
return new TileCollection([new ChargingTile($carProperty->value)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php
Normal file
21
src/Domain/Search/TileBuilder/ConsumptionTileBuilder.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\ConsumptionTile;
|
||||||
|
|
||||||
|
final readonly class ConsumptionTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
// Implementation would need to extract consumption data from the CarProperty
|
||||||
|
// This is a placeholder - you'll need to implement the actual logic
|
||||||
|
// based on how CarProperty contains consumption information
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Domain/Search/TileBuilder/DrivetrainTileBuilder.php
Normal file
20
src/Domain/Search/TileBuilder/DrivetrainTileBuilder.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
|
final readonly class DrivetrainTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
// Implementation would need to extract drivetrain data from the CarProperty
|
||||||
|
// This is a placeholder - you'll need to implement the actual logic
|
||||||
|
// based on how CarProperty contains drivetrain information
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Domain/Search/TileBuilder/PowerTileBuilder.php
Normal file
20
src/Domain/Search/TileBuilder/PowerTileBuilder.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
|
final readonly class PowerTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
// Implementation would need to extract power data from the CarProperty
|
||||||
|
// This is a placeholder - you'll need to implement the actual logic
|
||||||
|
// based on how CarProperty contains power information
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Domain/Search/TileBuilder/PriceTileBuilder.php
Normal file
23
src/Domain/Search/TileBuilder/PriceTileBuilder.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CatalogPrice;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\PriceTile;
|
||||||
|
|
||||||
|
final readonly class PriceTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if (!$carProperty->value instanceof CatalogPrice) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileCollection([new PriceTile($carProperty->value->price)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\Brand;
|
||||||
|
use App\Domain\Model\Cars\CarModel;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
|
use App\Domain\Model\Value\Date;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\ProductionPeriodTile;
|
||||||
|
|
||||||
|
final readonly class ProductionPeriodTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Brand $brand
|
||||||
|
* @param CarModel $carModel
|
||||||
|
* @param CarRevision $carRevision
|
||||||
|
* @param CarProperty $carProperty
|
||||||
|
*
|
||||||
|
* @return ProductionPeriodTile|null
|
||||||
|
*/
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
// Implementation would need to extract production period data from the CarProperty
|
||||||
|
// This is a placeholder - you'll need to implement the actual logic
|
||||||
|
// based on how CarProperty contains production period information
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/Domain/Search/TileBuilder/RangeTileBuilder.php
Normal file
20
src/Domain/Search/TileBuilder/RangeTileBuilder.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\RangeTile;
|
||||||
|
|
||||||
|
final readonly class RangeTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if ($carProperty->value instanceof RangeSpecification) {
|
||||||
|
return new TileCollection([new RangeTile($carProperty->value->nefzRange->range)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Domain/Search/TileBuilder/TileBuilder.php
Normal file
19
src/Domain/Search/TileBuilder/TileBuilder.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
|
#[AutoconfigureTag('app.tile_builder')]
|
||||||
|
interface TileBuilder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param CarProperty<CarPropertyValue> $carProperty
|
||||||
|
*
|
||||||
|
* @return TileCollection|null
|
||||||
|
*/
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection;
|
||||||
|
}
|
||||||
34
src/Domain/Search/TileBuilder/TileBuilderProvider.php
Normal file
34
src/Domain/Search/TileBuilder/TileBuilderProvider.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||||
|
|
||||||
|
final readonly class TileBuilderProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param iterable<TileBuilder> $tileBuilders
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[AutowireIterator('app.tile_builder')]
|
||||||
|
private iterable $tileBuilders,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param CarProperty<CarPropertyValue> $carProperty
|
||||||
|
*/
|
||||||
|
public function build(CarProperty $carProperty): TileCollection
|
||||||
|
{
|
||||||
|
foreach ($this->tileBuilders as $tileBuilder) {
|
||||||
|
$tile = $tileBuilder->build($carProperty);
|
||||||
|
if ($tile !== null) {
|
||||||
|
return $tile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception(sprintf('No tile builder found for car property %s of type %s', $carProperty->carPropertyId->value, get_class($carProperty->value)));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php
Normal file
21
src/Domain/Search/TileBuilder/TopSpeedTileBuilder.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\TileBuilder;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\TopSpeedTile;
|
||||||
|
|
||||||
|
final readonly class TopSpeedTileBuilder implements TileBuilder
|
||||||
|
{
|
||||||
|
|
||||||
|
public function build(CarProperty $carProperty): ?TileCollection
|
||||||
|
{
|
||||||
|
if ($carProperty->value instanceof TopSpeed) {
|
||||||
|
return new TileCollection([new TopSpeedTile($carProperty->value->speed)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,61 +0,0 @@
|
|||||||
<?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::DRIVING_CHARACTERISTICS_ACCELERATION,
|
|
||||||
])) {
|
|
||||||
$priceProperty = $carProperties->getOne(CarPropertyType::CATALOG_PRICE);
|
|
||||||
$accelerationProperty = $carProperties->getOne(CarPropertyType::DRIVING_CHARACTERISTICS_ACCELERATION);
|
|
||||||
|
|
||||||
if ($priceProperty !== null && $accelerationProperty !== null && $carRevision->image !== 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(
|
|
||||||
$carRevision->image,
|
|
||||||
[
|
|
||||||
new PriceTile($priceValue),
|
|
||||||
new AccelerationTile($accelerationValue),
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$tiles->add(new SectionTile(
|
|
||||||
$carRevision->name,
|
|
||||||
$subTiles->array()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -23,4 +23,9 @@ final class TileCollection
|
|||||||
{
|
{
|
||||||
$this->tiles[] = $tile;
|
$this->tiles[] = $tile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function merge(TileCollection $tileCollection): void
|
||||||
|
{
|
||||||
|
$this->tiles = array_merge($this->tiles, $tileCollection->array());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Acceleration;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
|
||||||
class AccelerationTile
|
class AccelerationTile
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use App\Domain\Model\Value\Date;
|
|||||||
class AvailabilityTile
|
class AvailabilityTile
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $status,
|
|
||||||
public readonly ?Date $availableSince = null,
|
public readonly ?Date $availableSince = null,
|
||||||
|
public readonly ?Date $availableUntil = null,
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@ -10,7 +10,7 @@ final readonly class CarTile
|
|||||||
* @param object[] $tiles
|
* @param object[] $tiles
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public Image $image,
|
public ?Image $image,
|
||||||
public array $tiles
|
public array $tiles
|
||||||
) { }
|
) { }
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles;
|
||||||
|
|
||||||
use App\Domain\Model\Value\ChargingSpeed;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargingSpeed;
|
||||||
|
|
||||||
class ChargingTile
|
class ChargingTile
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,11 +4,8 @@ namespace App\Domain\Search\Tiles;
|
|||||||
|
|
||||||
class SectionTile
|
class SectionTile
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param object[] $tiles
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $title,
|
public readonly string $title,
|
||||||
public readonly array $tiles,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@ -4,11 +4,7 @@ namespace App\Domain\Search\Tiles;
|
|||||||
|
|
||||||
class SubSectionTile
|
class SubSectionTile
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @param object[] $tiles
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $title,
|
public readonly string $title
|
||||||
public readonly array $tiles,
|
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
31
src/Domain/Search/View/CarRevisionComparison.php
Normal file
31
src/Domain/Search/View/CarRevisionComparison.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
|
final readonly class CarRevisionComparison implements View
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection
|
||||||
|
{
|
||||||
|
return new TileCollection([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDescription(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'car_revision_id_1' => 'Car revision ID 1',
|
||||||
|
'car_revision_id_2' => 'Car revision ID 2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'This view shows a comparison of two car revisions.';
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/Domain/Search/View/FullBrandView.php
Normal file
33
src/Domain/Search/View/FullBrandView.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
|
final readonly class FullBrandView implements View
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection
|
||||||
|
{
|
||||||
|
return new TileCollection([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDescription(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'brand_id' => 'Brand ID',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return <<<'EOT'
|
||||||
|
This view shows all information about a brand. It is used to display the full information about a brand if requested in the query.
|
||||||
|
E.g. a brand name is given. You should only use this view if you are really sure, that the query is about a brand and not models or revisions.
|
||||||
|
EOT;
|
||||||
|
}
|
||||||
|
}
|
||||||
93
src/Domain/Search/View/FullCarModelView.php
Normal file
93
src/Domain/Search/View/FullCarModelView.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Id\CarModelId;
|
||||||
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
|
use App\Domain\Repository\CarModelRepository;
|
||||||
|
use App\Domain\Repository\Loader\FullCarLoader;
|
||||||
|
use App\Domain\Repository\Loader\FullCarRevisionLoader;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\TileBuilder\TileBuilderProvider;
|
||||||
|
use App\Domain\Search\Tiles\CarTile;
|
||||||
|
use App\Domain\Search\Tiles\SectionTile;
|
||||||
|
use App\Domain\Search\Tiles\SubSectionTile;
|
||||||
|
|
||||||
|
final readonly class FullCarModelView implements View
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FullCarLoader $fullCarLoader,
|
||||||
|
private readonly TileBuilderProvider $tileBuilderProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection
|
||||||
|
{
|
||||||
|
if (!is_string($data['car_model_id'] ?? null)) {
|
||||||
|
throw new \InvalidArgumentException('Car model ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullCarModel = $this->fullCarLoader->loadModel(new CarModelId($data['car_model_id']));
|
||||||
|
$carModel = $fullCarModel->getCarModel();
|
||||||
|
$brand = $fullCarModel->getBrand();
|
||||||
|
$carRevisions = $fullCarModel->getCarRevisions();
|
||||||
|
|
||||||
|
$allTiles = [];
|
||||||
|
|
||||||
|
// Add section title for the car model
|
||||||
|
$allTiles[] = new SectionTile($brand->name . ' ' . $carModel->name);
|
||||||
|
|
||||||
|
// Generate a CarTile for each car revision
|
||||||
|
foreach ($carRevisions as $fullCarRevision) {
|
||||||
|
$carProperties = $fullCarRevision->getCarPropertyCollection();
|
||||||
|
$carRevision = $fullCarRevision->getCarRevision();
|
||||||
|
|
||||||
|
/** @var CarProperty<CarPropertyValue>[] $properties */
|
||||||
|
$properties = array_filter([
|
||||||
|
$carProperties->getOne(Production::class),
|
||||||
|
$carProperties->getOne(TopSpeed::class),
|
||||||
|
$carProperties->getOne(Acceleration::class),
|
||||||
|
$carProperties->getOne(RangeSpecification::class),
|
||||||
|
], static fn($value) => $value !== null);
|
||||||
|
|
||||||
|
$tiles = new TileCollection([]);
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$tileCollection = $this->tileBuilderProvider->build($property);
|
||||||
|
$tiles->merge($tileCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
$allTiles[] = new SubSectionTile($brand->name . ' ' . $carModel->name . ' ' . $carRevision->name);
|
||||||
|
$allTiles[] = new CarTile(
|
||||||
|
$carRevision->image,
|
||||||
|
$tiles->array(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileCollection($allTiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDescription(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'car_model_id' => 'Car model ID',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return <<<'EOT'
|
||||||
|
This view shows all information about a car model. It is used to display the full information about a car model if requested by the user.
|
||||||
|
E.g. a model name is given
|
||||||
|
EOT;
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/Domain/Search/View/FullCarRevisionView.php
Normal file
81
src/Domain/Search/View/FullCarRevisionView.php
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
||||||
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
|
use App\Domain\Repository\Loader\FullCarLoader;
|
||||||
|
use App\Domain\Repository\Loader\FullCarRevisionLoader;
|
||||||
|
use App\Domain\Search\TileBuilder\TileBuilderProvider;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\CarTile;
|
||||||
|
use App\Domain\Search\Tiles\SectionTile;
|
||||||
|
|
||||||
|
final readonly class FullCarRevisionView implements View
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly FullCarLoader $fullCarLoader,
|
||||||
|
private readonly TileBuilderProvider $tileBuilderProvider
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection
|
||||||
|
{
|
||||||
|
if (!is_string($data['car_revision_id'] ?? null)) {
|
||||||
|
throw new \InvalidArgumentException('Car revision ID is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullCar = $this->fullCarLoader->loadRevision(new CarRevisionId($data['car_revision_id']));
|
||||||
|
|
||||||
|
$carRevision = $fullCar->getCarRevision();
|
||||||
|
$carModel = $fullCar->getCarModel();
|
||||||
|
$brand = $fullCar->getBrand();
|
||||||
|
$carProperties = $fullCar->getCarPropertyCollection();
|
||||||
|
|
||||||
|
/** @var CarProperty<CarPropertyValue>[] $properties */
|
||||||
|
$properties = array_filter([
|
||||||
|
$carProperties->getOne(Production::class),
|
||||||
|
$carProperties->getOne(TopSpeed::class),
|
||||||
|
$carProperties->getOne(Acceleration::class),
|
||||||
|
$carProperties->getOne(RangeSpecification::class),
|
||||||
|
], static fn($value) => $value !== null);
|
||||||
|
|
||||||
|
$tiles = new TileCollection([]);
|
||||||
|
foreach ($properties as $property) {
|
||||||
|
$tileCollection = $this->tileBuilderProvider->build($property);
|
||||||
|
$tiles->merge($tileCollection);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileCollection([
|
||||||
|
new SectionTile($brand->name . ' ' . $carModel->name . ' ' . $carRevision->name),
|
||||||
|
new CarTile(
|
||||||
|
$carRevision->image,
|
||||||
|
$tiles->array(),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDescription(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'car_revision_id' => 'Car revision ID',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return <<<'EOT'
|
||||||
|
This view shows all information about a car revision. It is used to display the full information about a car revision if a specific revision is requested in the query.
|
||||||
|
E.g. a model and revision name is given. You should always prefer this view over the full car model view if enough information is given in the query.
|
||||||
|
EOT;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Domain/Search/View/SpecificCarPropertyView.php
Normal file
72
src/Domain/Search/View/SpecificCarPropertyView.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
|
use App\Domain\Repository\BrandRepository;
|
||||||
|
use App\Domain\Repository\CarModelRepository;
|
||||||
|
use App\Domain\Repository\CarPropertyRepository;
|
||||||
|
use App\Domain\Repository\CarRevisionRepository;
|
||||||
|
use App\Domain\Repository\Loader\FullCarLoader;
|
||||||
|
use App\Domain\Search\TileBuilder\TileBuilderProvider;
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\SectionTile;
|
||||||
|
|
||||||
|
final readonly class SpecificCarPropertyView implements View
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly TileBuilderProvider $tileBuilderProvider,
|
||||||
|
private readonly FullCarLoader $fullCarLoader,
|
||||||
|
private readonly CarPropertyRepository $carPropertyRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection
|
||||||
|
{
|
||||||
|
$properties = $data['properties'] ?? [];
|
||||||
|
if (!is_array($properties)) {
|
||||||
|
throw new \Exception('Properties must be an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
$tiles = [];
|
||||||
|
foreach ($properties as $propertyId) {
|
||||||
|
if (!is_string($propertyId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$carProperty = $this->carPropertyRepository->findById(new CarPropertyId($propertyId));
|
||||||
|
if ($carProperty === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fullCar = $this->fullCarLoader->loadRevisionByCarPropertyId($carProperty->carPropertyId);
|
||||||
|
|
||||||
|
$tileCollection = $this->tileBuilderProvider->build($carProperty);
|
||||||
|
|
||||||
|
$tiles[] = new SectionTile($fullCar->getBrand()->name . ' ' . $fullCar->getCarModel()->name . ' ' . $fullCar->getCarRevision()->name);
|
||||||
|
$tiles = array_merge($tiles, $tileCollection->array());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileCollection($tiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dataDescription(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'properties' => [
|
||||||
|
'carproperty_123',
|
||||||
|
'carproperty_456',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function description(): string
|
||||||
|
{
|
||||||
|
return 'This view shows all information about a specific car property.';
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Domain/Search/View/View.php
Normal file
27
src/Domain/Search/View/View.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use App\Domain\Search\TileCollection;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
|
#[AutoconfigureTag('app.view')]
|
||||||
|
interface View
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<mixed> $data
|
||||||
|
*
|
||||||
|
* @return TileCollection
|
||||||
|
*/
|
||||||
|
public function build(array $data): TileCollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function dataDescription(): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function description(): string;
|
||||||
|
}
|
||||||
43
src/Domain/Search/View/ViewProvider.php
Normal file
43
src/Domain/Search/View/ViewProvider.php
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||||
|
|
||||||
|
final readonly class ViewProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param iterable<View> $views
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
#[AutowireIterator('app.view')]
|
||||||
|
private iterable $views,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $viewClass
|
||||||
|
*
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function getView(string $viewClass): View
|
||||||
|
{
|
||||||
|
foreach ($this->views as $view) {
|
||||||
|
$reflectionClass = new \ReflectionClass($view);
|
||||||
|
$shortClassName = $reflectionClass->getShortName();
|
||||||
|
|
||||||
|
if ($shortClassName === $viewClass) {
|
||||||
|
return $view;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception(sprintf('View %s not found', $viewClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<View>
|
||||||
|
*/
|
||||||
|
public function getAllViews(): array
|
||||||
|
{
|
||||||
|
return iterator_to_array($this->views);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
||||||
|
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
|
|
||||||
class ModelMapper
|
class ModelMapper
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\BrandRepository;
|
||||||
|
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\BrandCollection;
|
use App\Domain\Model\Cars\BrandCollection;
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
use App\Domain\Repository\BrandRepository;
|
use App\Domain\Repository\BrandRepository;
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
||||||
|
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\CarModelRepository;
|
||||||
|
|
||||||
use App\Domain\Model\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\CarModelCollection;
|
use App\Domain\Model\Cars\CarModelCollection;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
use App\Domain\Repository\CarModelRepository;
|
use App\Domain\Repository\CarModelRepository;
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\CarPropertyRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\CarPropertyRepository;
|
||||||
|
|
||||||
use App\Domain\Model\CarProperty;
|
use App\Domain\Model\Cars\CarPropertyCollection;
|
||||||
use App\Domain\Model\CarPropertyCollection;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\CarPropertyType;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\CarPropertyId;
|
use App\Domain\Model\Id\CarPropertyId;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Repository\CarPropertyRepository;
|
use App\Domain\Repository\CarPropertyRepository;
|
||||||
@ -17,6 +17,21 @@ final class SqlCarPropertyRepository implements CarPropertyRepository
|
|||||||
private readonly Connection $connection,
|
private readonly Connection $connection,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public function findById(CarPropertyId $carPropertyId): ?CarProperty
|
||||||
|
{
|
||||||
|
$result = $this->connection->executeQuery(
|
||||||
|
'SELECT * FROM car_properties WHERE id = ?',
|
||||||
|
[$carPropertyId->value]
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = $result->fetchAssociative();
|
||||||
|
if ($row === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->mapRowToCarProperty($row);
|
||||||
|
}
|
||||||
|
|
||||||
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection
|
public function findByCarRevision(CarRevision $carRevision): CarPropertyCollection
|
||||||
{
|
{
|
||||||
$result = $this->connection->executeQuery(
|
$result = $this->connection->executeQuery(
|
||||||
@ -37,8 +52,20 @@ final class SqlCarPropertyRepository implements CarPropertyRepository
|
|||||||
|
|
||||||
public function findByEmbeddingIds(array $embeddingIds): CarPropertyCollection
|
public function findByEmbeddingIds(array $embeddingIds): CarPropertyCollection
|
||||||
{
|
{
|
||||||
// Placeholder implementation - would need to join with embeddings table
|
$result = $this->connection->executeQuery(
|
||||||
return new CarPropertyCollection([]);
|
'SELECT * FROM car_properties WHERE id IN (?)',
|
||||||
|
[$embeddingIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
$carProperties = [];
|
||||||
|
foreach ($result->fetchAllAssociative() as $row) {
|
||||||
|
$carProperty = $this->mapRowToCarProperty($row);
|
||||||
|
if ($carProperty !== null) {
|
||||||
|
$carProperties[] = $carProperty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CarPropertyCollection($carProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function findByEmbeddingPhraseHashes(array $phraseHashes): CarPropertyCollection
|
public function findByEmbeddingPhraseHashes(array $phraseHashes): CarPropertyCollection
|
||||||
@ -71,11 +98,10 @@ final class SqlCarPropertyRepository implements CarPropertyRepository
|
|||||||
{
|
{
|
||||||
$this->connection->transactional(function (Connection $connection) use ($carProperty) {
|
$this->connection->transactional(function (Connection $connection) use ($carProperty) {
|
||||||
$connection->executeStatement(
|
$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',
|
'INSERT INTO car_properties (id, car_revision_id, value) VALUES (?, ?, ?) ON CONFLICT (id) DO UPDATE SET car_revision_id = EXCLUDED.car_revision_id, value = EXCLUDED.value',
|
||||||
[
|
[
|
||||||
$carProperty->carPropertyId->value,
|
$carProperty->carPropertyId->value,
|
||||||
$carProperty->carRevisionId->value,
|
$carProperty->carRevisionId->value,
|
||||||
$carProperty->type->value,
|
|
||||||
serialize($carProperty->value),
|
serialize($carProperty->value),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@ -115,25 +141,27 @@ final class SqlCarPropertyRepository implements CarPropertyRepository
|
|||||||
*/
|
*/
|
||||||
private function mapRowToCarProperty(array $row): ?CarProperty
|
private function mapRowToCarProperty(array $row): ?CarProperty
|
||||||
{
|
{
|
||||||
try {
|
$id = $row['id'] ?? null;
|
||||||
$id = $row['id'] ?? null;
|
$carRevisionId = $row['car_revision_id'] ?? null;
|
||||||
$carRevisionId = $row['car_revision_id'] ?? null;
|
$value = $row['value'] ?? null;
|
||||||
$type = $row['type'] ?? null;
|
|
||||||
$value = $row['value'] ?? null;
|
|
||||||
|
|
||||||
if (!is_string($id) || !is_string($carRevisionId) || !is_string($type) || !is_string($value)) {
|
if (!is_string($id) || !is_string($carRevisionId) || !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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$value = unserialize($value);
|
||||||
|
if (!$value instanceof CarPropertyValue) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CarProperty(
|
||||||
|
new CarPropertyId($id),
|
||||||
|
new CarRevisionId($carRevisionId),
|
||||||
|
$value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
||||||
|
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Model\Image;
|
use App\Domain\Model\Image;
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
namespace App\Infrastructure\PostgreSQL\Repository\CarRevisionRepository;
|
||||||
|
|
||||||
use App\Domain\Model\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\CarRevisionCollection;
|
use App\Domain\Model\Cars\CarRevisionCollection;
|
||||||
use App\Domain\Model\Id\CarRevisionId;
|
use App\Domain\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
use App\Domain\Model\Id\CarModelId;
|
||||||
use App\Domain\Repository\CarRevisionRepository;
|
use App\Domain\Repository\CarRevisionRepository;
|
||||||
|
|||||||
@ -66,7 +66,7 @@ final class SqlEmbeddingRepository implements EmbeddingRepository
|
|||||||
return $this->mapRowToEmbedding($row);
|
return $this->mapRowToEmbedding($row);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 100): EmbeddingCollection
|
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 20): EmbeddingCollection
|
||||||
{
|
{
|
||||||
$result = $this->connection->executeQuery(
|
$result = $this->connection->executeQuery(
|
||||||
'SELECT *, large_embedding_vector <=> :embeddingVector AS distance
|
'SELECT *, large_embedding_vector <=> :embeddingVector AS distance
|
||||||
@ -82,15 +82,13 @@ final class SqlEmbeddingRepository implements EmbeddingRepository
|
|||||||
|
|
||||||
$embeddings = [];
|
$embeddings = [];
|
||||||
foreach ($result->fetchAllAssociative() as $row) {
|
foreach ($result->fetchAllAssociative() as $row) {
|
||||||
if ($row['distance'] < 0.7) {
|
$embeddings[] = $this->mapRowToEmbedding($row);
|
||||||
$embeddings[] = $this->mapRowToEmbedding($row);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EmbeddingCollection($embeddings);
|
return new EmbeddingCollection($embeddings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 10): EmbeddingCollection
|
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 20): EmbeddingCollection
|
||||||
{
|
{
|
||||||
$result = $this->connection->executeQuery(
|
$result = $this->connection->executeQuery(
|
||||||
'SELECT *
|
'SELECT *
|
||||||
|
|||||||
13
symfony.lock
13
symfony.lock
@ -173,5 +173,18 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"config/packages/validator.yaml"
|
"config/packages/validator.yaml"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"symfony/web-profiler-bundle": {
|
||||||
|
"version": "7.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "5b2b543e13942495c0003f67780cb4448af9e606"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/web_profiler.yaml",
|
||||||
|
"config/routes/web_profiler.yaml"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -230,7 +230,7 @@
|
|||||||
.subsection-title {
|
.subsection-title {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.25rem 0rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 2px 2px 0 0;
|
border-radius: 2px 2px 0 0;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
|
|||||||
39
templates/profiler/ai-chat.html.twig
Normal file
39
templates/profiler/ai-chat.html.twig
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{# templates/Collector/ai_chat.html.twig #}
|
||||||
|
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block toolbar %}
|
||||||
|
{# Optional: Add a toolbar icon or summary here #}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block menu %}
|
||||||
|
<span class="label">
|
||||||
|
<span class="icon">{{ include('@WebProfiler/Icon/logger.svg') }}</span>
|
||||||
|
<strong>AI Chat</strong>
|
||||||
|
</span>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block panel %}
|
||||||
|
<h2>AI Chat Log</h2>
|
||||||
|
{% if collector.log is empty %}
|
||||||
|
<div class="empty">
|
||||||
|
<p>No AI chat log entries.</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Prompt</th>
|
||||||
|
<th>Response</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in collector.log %}
|
||||||
|
<tr>
|
||||||
|
<td><pre>{{ entry.prompt|e }}</pre></td>
|
||||||
|
<td><pre>{{ entry.response|e }}</pre></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
@ -1,6 +1,10 @@
|
|||||||
<div style="grid-column: span 2; grid-row: span 2">
|
<div style="grid-column: span 2; grid-row: span 2">
|
||||||
{% if tile.image %}
|
{% if tile.image %}
|
||||||
<img src="{{ tile.image.externalPublicUrl }}" style="width: 100%; height: 100%; object-fit: cover;">
|
<img src="{{ tile.image.externalPublicUrl }}" style="width: 100%; height: 100%; object-fit: cover;">
|
||||||
|
{% else %}
|
||||||
|
<div style="width: 100%; height: 100%; background-color: #f0f0f0; display: flex; align-items: center; justify-content: center; color: #666;">
|
||||||
|
<i class="fas fa-car" style="font-size: 3rem;"></i>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% for tile in tile.tiles %}
|
{% for tile in tile.tiles %}
|
||||||
|
|||||||
@ -1,41 +1,24 @@
|
|||||||
<div class="tile production-period-tile">
|
<div class="tile production-period-tile">
|
||||||
<div class="tile-title">
|
<div class="tile-title">
|
||||||
<i class="fas fa-industry" style="color: #6c757d; margin-right: 8px;"></i>
|
|
||||||
Produktionszeitraum
|
|
||||||
</div>
|
|
||||||
<div class="production-timeline" style="margin-top: 8px;">
|
|
||||||
{% if tile.productionBegin or tile.productionEnd %}
|
{% if tile.productionBegin or tile.productionEnd %}
|
||||||
<div style="display: flex; align-items: center; gap: 8px;">
|
<div style="font-size: 18px; font-weight: bold; margin-bottom: 4px;">
|
||||||
{% if tile.productionBegin %}
|
{% if tile.productionBegin %}
|
||||||
<div style="text-align: center;">
|
{{ tile.productionBegin.year }}
|
||||||
<div style="font-weight: bold; font-size: 16px; color: #28a745;">{{ tile.productionBegin.year }}</div>
|
{% else %}
|
||||||
<small style="color: #666;">Start</small>
|
?
|
||||||
|
{% endif %}
|
||||||
|
bis
|
||||||
|
{% if tile.productionEnd %}
|
||||||
|
{{ tile.productionEnd.year }}
|
||||||
|
{% else %}
|
||||||
|
heute
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if tile.productionBegin and tile.productionEnd %}
|
|
||||||
<div style="flex: 1; height: 2px; background: linear-gradient(to right, #28a745, #dc3545); margin: 0 4px;"></div>
|
|
||||||
{% elseif tile.productionBegin %}
|
|
||||||
<div style="flex: 1; height: 2px; background: linear-gradient(to right, #28a745, #007acc); margin: 0 4px;"></div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if tile.productionEnd %}
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<div style="font-weight: bold; font-size: 16px; color: #dc3545;">{{ tile.productionEnd.year }}</div>
|
|
||||||
<small style="color: #666;">Ende</small>
|
|
||||||
</div>
|
|
||||||
{% elseif tile.productionBegin %}
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<div style="font-weight: bold; font-size: 16px; color: #007acc;">laufend</div>
|
|
||||||
<small style="color: #666;">aktuell</small>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div style="text-align: center; color: #666; font-style: italic;">
|
<div style="color: #666; font-style: italic;">
|
||||||
Zeitraum unbekannt
|
Zeitraum unbekannt
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<small style="color: #666;">Verfügbarkeit</small>
|
<small>Produktion</small>
|
||||||
</div>
|
</div>
|
||||||
@ -1,3 +1 @@
|
|||||||
<h1 class="section-title" style="grid-column: span 5;">{{ tile.title }}</h1>
|
<h1 class="section-title" style="grid-column: span 5;">{{ tile.title }}</h1>
|
||||||
|
|
||||||
{% include 'result/tiles/collection.html.twig' with { tiles: tile.tiles } %}
|
|
||||||
@ -1,2 +1 @@
|
|||||||
<h2 class="subsection-title" style="grid-column: span 5">{{ tile.title }}</h2>
|
<h2 class="subsection-title" style="grid-column: span 5">{{ tile.title }}</h2>
|
||||||
{% include 'result/tiles/collection.html.twig' with { tiles: tile.tiles } %}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user