refactored
This commit is contained in:
parent
3f78e2e9f1
commit
7051bbf5b7
@ -19,6 +19,7 @@
|
||||
"symfony/dotenv": "7.2.*",
|
||||
"symfony/flex": "^2",
|
||||
"symfony/framework-bundle": "7.2.*",
|
||||
"symfony/monolog-bundle": "^3.10",
|
||||
"symfony/runtime": "7.2.*",
|
||||
"symfony/serializer": "7.2.*",
|
||||
"symfony/twig-bundle": "7.2.*",
|
||||
|
||||
438
composer.lock
generated
438
composer.lock
generated
@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e4da23c3811aae55314b0e018026605a",
|
||||
"content-hash": "45fe64d8d59b093a4dc302486a3a607d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "composer/semver",
|
||||
@ -792,6 +792,109 @@
|
||||
},
|
||||
"time": "2025-01-24T11:45:48+00:00"
|
||||
},
|
||||
{
|
||||
"name": "monolog/monolog",
|
||||
"version": "3.9.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Seldaek/monolog.git",
|
||||
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
||||
"reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=8.1",
|
||||
"psr/log": "^2.0 || ^3.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/log-implementation": "3.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"aws/aws-sdk-php": "^3.0",
|
||||
"doctrine/couchdb": "~1.0@dev",
|
||||
"elasticsearch/elasticsearch": "^7 || ^8",
|
||||
"ext-json": "*",
|
||||
"graylog2/gelf-php": "^1.4.2 || ^2.0",
|
||||
"guzzlehttp/guzzle": "^7.4.5",
|
||||
"guzzlehttp/psr7": "^2.2",
|
||||
"mongodb/mongodb": "^1.8",
|
||||
"php-amqplib/php-amqplib": "~2.4 || ^3",
|
||||
"php-console/php-console": "^3.1.8",
|
||||
"phpstan/phpstan": "^2",
|
||||
"phpstan/phpstan-deprecation-rules": "^2",
|
||||
"phpstan/phpstan-strict-rules": "^2",
|
||||
"phpunit/phpunit": "^10.5.17 || ^11.0.7",
|
||||
"predis/predis": "^1.1 || ^2",
|
||||
"rollbar/rollbar": "^4.0",
|
||||
"ruflin/elastica": "^7 || ^8",
|
||||
"symfony/mailer": "^5.4 || ^6",
|
||||
"symfony/mime": "^5.4 || ^6"
|
||||
},
|
||||
"suggest": {
|
||||
"aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
|
||||
"doctrine/couchdb": "Allow sending log messages to a CouchDB server",
|
||||
"elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
|
||||
"ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
|
||||
"ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
|
||||
"ext-mbstring": "Allow to work properly with unicode symbols",
|
||||
"ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
|
||||
"ext-openssl": "Required to send log messages using SSL",
|
||||
"ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
|
||||
"graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
|
||||
"mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
|
||||
"php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
|
||||
"rollbar/rollbar": "Allow sending log messages to Rollbar",
|
||||
"ruflin/elastica": "Allow sending log messages to an Elastic Search server"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-main": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Monolog\\": "src/Monolog"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jordi Boggiano",
|
||||
"email": "j.boggiano@seld.be",
|
||||
"homepage": "https://seld.be"
|
||||
}
|
||||
],
|
||||
"description": "Sends your logs to files, sockets, inboxes, databases and various web services",
|
||||
"homepage": "https://github.com/Seldaek/monolog",
|
||||
"keywords": [
|
||||
"log",
|
||||
"logging",
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/Seldaek/monolog/issues",
|
||||
"source": "https://github.com/Seldaek/monolog/tree/3.9.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/Seldaek",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-03-24T10:02:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nyholm/psr7",
|
||||
"version": "1.8.2",
|
||||
@ -3157,6 +3260,165 @@
|
||||
],
|
||||
"time": "2025-05-29T07:47:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/monolog-bridge",
|
||||
"version": "v7.3.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/monolog-bridge.git",
|
||||
"reference": "1b188c8abbbef25b111da878797514b7a8d33990"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/monolog-bridge/zipball/1b188c8abbbef25b111da878797514b7a8d33990",
|
||||
"reference": "1b188c8abbbef25b111da878797514b7a8d33990",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"monolog/monolog": "^3",
|
||||
"php": ">=8.2",
|
||||
"symfony/http-kernel": "^6.4|^7.0",
|
||||
"symfony/service-contracts": "^2.5|^3"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/console": "<6.4",
|
||||
"symfony/http-foundation": "<6.4",
|
||||
"symfony/security-core": "<6.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/http-client": "^6.4|^7.0",
|
||||
"symfony/mailer": "^6.4|^7.0",
|
||||
"symfony/messenger": "^6.4|^7.0",
|
||||
"symfony/mime": "^6.4|^7.0",
|
||||
"symfony/security-core": "^6.4|^7.0",
|
||||
"symfony/var-dumper": "^6.4|^7.0"
|
||||
},
|
||||
"type": "symfony-bridge",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bridge\\Monolog\\": ""
|
||||
},
|
||||
"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 integration for Monolog with various Symfony components",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/monolog-bridge/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-03-21T12:17:46+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/monolog-bundle",
|
||||
"version": "v3.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/monolog-bundle.git",
|
||||
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
|
||||
"reference": "414f951743f4aa1fd0f5bf6a0e9c16af3fe7f181",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"monolog/monolog": "^1.25.1 || ^2.0 || ^3.0",
|
||||
"php": ">=7.2.5",
|
||||
"symfony/config": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/dependency-injection": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/http-kernel": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/monolog-bridge": "^5.4 || ^6.0 || ^7.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/console": "^5.4 || ^6.0 || ^7.0",
|
||||
"symfony/phpunit-bridge": "^6.3 || ^7.0",
|
||||
"symfony/yaml": "^5.4 || ^6.0 || ^7.0"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Bundle\\MonologBundle\\": ""
|
||||
},
|
||||
"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": "Symfony MonologBundle",
|
||||
"homepage": "https://symfony.com",
|
||||
"keywords": [
|
||||
"log",
|
||||
"logging"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/symfony/monolog-bundle/issues",
|
||||
"source": "https://github.com/symfony/monolog-bundle/tree/v3.10.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": "2023-11-06T17:08:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.32.0",
|
||||
@ -4572,91 +4834,6 @@
|
||||
],
|
||||
"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",
|
||||
"version": "v7.2.6",
|
||||
@ -5314,11 +5491,96 @@
|
||||
}
|
||||
],
|
||||
"time": "2025-04-17T09:11:12+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"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": true,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
@ -5328,6 +5590,6 @@
|
||||
"ext-pdo": "*",
|
||||
"ext-pgsql": "*"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.6.0"
|
||||
}
|
||||
|
||||
@ -8,4 +8,5 @@ return [
|
||||
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
|
||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
62
config/packages/monolog.yaml
Normal file
62
config/packages/monolog.yaml
Normal file
@ -0,0 +1,62 @@
|
||||
monolog:
|
||||
channels:
|
||||
- deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
|
||||
|
||||
when@dev:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
channels: ["!event"]
|
||||
# uncomment to get logging in your browser
|
||||
# you may have to allow bigger header sizes in your Web server configuration
|
||||
#firephp:
|
||||
# type: firephp
|
||||
# level: info
|
||||
#chromephp:
|
||||
# type: chromephp
|
||||
# level: info
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine", "!console"]
|
||||
|
||||
when@test:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
channels: ["!event"]
|
||||
nested:
|
||||
type: stream
|
||||
path: "%kernel.logs_dir%/%kernel.environment%.log"
|
||||
level: debug
|
||||
|
||||
when@prod:
|
||||
monolog:
|
||||
handlers:
|
||||
main:
|
||||
type: fingers_crossed
|
||||
action_level: error
|
||||
handler: nested
|
||||
excluded_http_codes: [404, 405]
|
||||
buffer_size: 50 # How many messages should be saved? Prevent memory leaks
|
||||
nested:
|
||||
type: stream
|
||||
path: php://stderr
|
||||
level: debug
|
||||
formatter: monolog.formatter.json
|
||||
console:
|
||||
type: console
|
||||
process_psr_3_messages: false
|
||||
channels: ["!event", "!doctrine"]
|
||||
deprecation:
|
||||
type: stream
|
||||
channels: [deprecation]
|
||||
path: php://stderr
|
||||
formatter: monolog.formatter.json
|
||||
30
migrations/Version20250609160739.php
Normal file
30
migrations/Version20250609160739.php
Normal file
@ -0,0 +1,30 @@
|
||||
<?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 Version20250609160739 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add embeddings to car revisions';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE car_revisions ADD COLUMN embeddings JSONB');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace App\Application\Commands;
|
||||
|
||||
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
||||
use App\Domain\ContentManagement\CarRevisionEmbedder;
|
||||
use App\Domain\Model\Cars\Brand;
|
||||
use App\Domain\Model\Cars\CarModel;
|
||||
use App\Domain\Model\Cars\CarRevision;
|
||||
@ -62,6 +63,7 @@ class LoadFixtures extends Command
|
||||
private readonly CarRevisionRepository $carRevisionRepository,
|
||||
private readonly CarPropertyRepository $carPropertyRepository,
|
||||
private readonly CarPropertyEmbedder $carPropertyEmbedder,
|
||||
private readonly CarRevisionEmbedder $carRevisionEmbedder,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
@ -154,6 +156,7 @@ class LoadFixtures extends Command
|
||||
$image
|
||||
);
|
||||
|
||||
$this->carRevisionEmbedder->createPersistedEmbedding($carRevision, $carModel, $brand);
|
||||
$this->carRevisionRepository->save($carRevision);
|
||||
|
||||
foreach ($revisionFixture['properties'] as $propertyValue) {
|
||||
@ -399,6 +402,22 @@ class LoadFixtures extends Command
|
||||
new RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(602))),
|
||||
new CatalogPrice(new Price(129990, Currency::euro())),
|
||||
]
|
||||
],
|
||||
[
|
||||
'revision' => 'Standard Range',
|
||||
'image' => new Image(externalPublicUrl: 'https://digitalassets.tesla.com/tesla-contents/image/upload/f_auto,q_auto/Model-3-Main-Hero-Desktop-LHD.jpg'),
|
||||
'properties' => [
|
||||
new Production(productionBegin: new Date(1, 1, 2020)),
|
||||
new MotorPower(new Power(220)),
|
||||
new Acceleration(6.7),
|
||||
new TopSpeed(new Speed(150)),
|
||||
new AverageConsumption(new Consumption(new Energy(15.5))),
|
||||
new BatteryCapacity(new Energy(60.0), new Energy(66.0)),
|
||||
new BatteryType(CellChemistry::LithiumNickelManganeseOxide, '2170', 'Tesla'),
|
||||
new ChargingSpeed(new Power(120), new Power(120)),
|
||||
new RangeSpecification(new NefzRange(new Range(450)), new WltpRange(new Range(450))),
|
||||
new CatalogPrice(new Price(49990, Currency::euro())),
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
@ -17,11 +17,21 @@ class SearchController extends AbstractController
|
||||
#[Route('/s/{query}', name: 'search')]
|
||||
public function index(string $query): Response
|
||||
{
|
||||
$decodedQuery = urldecode(str_replace('+', ' ', $query));
|
||||
$decodedQuery = urldecode($query);
|
||||
|
||||
return $this->render('result/index.html.twig', [
|
||||
'tiles' => $this->engine->search($decodedQuery)->array(),
|
||||
'query' => $decodedQuery,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/result/{query}', name: 'result')]
|
||||
public function result(string $query): Response
|
||||
{
|
||||
$decodedQuery = urldecode($query);
|
||||
|
||||
return $this->render('_components/result.html.twig', [
|
||||
'tiles' => $this->engine->search($decodedQuery)->array(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -45,7 +45,7 @@ class AIClient
|
||||
public function generateJson(string $prompt): array
|
||||
{
|
||||
$response = $this->client->chat()->create([
|
||||
'model' => 'gpt-4.1-nano',
|
||||
'model' => 'gpt-4.1-mini',
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
],
|
||||
|
||||
@ -12,6 +12,7 @@ use App\Domain\Model\Cars\CarRevision;
|
||||
use App\Domain\Model\Id\EmbeddingId;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
|
||||
|
||||
class CarPropertyEmbedder
|
||||
{
|
||||
public function __construct(
|
||||
@ -20,6 +21,13 @@ class CarPropertyEmbedder
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CarProperty<CarPropertyValue> $carProperty
|
||||
* @param CarRevision|null $carRevision
|
||||
* @param CarModel|null $carModel
|
||||
* @param Brand|null $brand
|
||||
* @return Embedding|null
|
||||
*/
|
||||
public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?CarModel $carModel, ?Brand $brand): ?Embedding
|
||||
{
|
||||
$text = $carProperty->value->humanReadable();
|
||||
|
||||
55
src/Domain/ContentManagement/CarRevisionEmbedder.php
Normal file
55
src/Domain/ContentManagement/CarRevisionEmbedder.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\ContentManagement;
|
||||
|
||||
use App\Domain\AI\AIClient;
|
||||
use App\Domain\Model\Embedding\Embedding;
|
||||
use App\Domain\Model\Cars\Brand;
|
||||
use App\Domain\Model\Cars\CarModel;
|
||||
use App\Domain\Model\Cars\CarRevision;
|
||||
use App\Domain\Model\Id\EmbeddingId;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
|
||||
|
||||
class CarRevisionEmbedder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AIClient $aiClient,
|
||||
private readonly EmbeddingRepository $embeddingRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CarRevision $carRevision
|
||||
* @param CarModel|null $carModel
|
||||
* @param Brand|null $brand
|
||||
* @return Embedding|null
|
||||
*/
|
||||
public function createPersistedEmbedding(CarRevision $carRevision, ?CarModel $carModel, ?Brand $brand): ?Embedding
|
||||
{
|
||||
$text = $carRevision->name;
|
||||
if ($carModel !== null) {
|
||||
$text .= ', ' . $carModel->name;
|
||||
}
|
||||
|
||||
if ($brand !== null) {
|
||||
$text .= ', ' . $brand->name;
|
||||
}
|
||||
|
||||
$embedding = $this->embeddingRepository->findByPhrase($text);
|
||||
if ($embedding) {
|
||||
$carRevision->embeddings->add($embedding->embeddingId);
|
||||
return $embedding;
|
||||
}
|
||||
|
||||
$largeEmbedding = $this->aiClient->embedTextLarge($text);
|
||||
$smallEmbedding = $this->aiClient->embedTextSmall($text);
|
||||
|
||||
$embedding = new Embedding(EmbeddingId::generate(), $text, $largeEmbedding, $smallEmbedding);
|
||||
$this->embeddingRepository->save($embedding);
|
||||
|
||||
$carRevision->embeddings->add($embedding->embeddingId);
|
||||
|
||||
return $embedding;
|
||||
}
|
||||
}
|
||||
17
src/Domain/Logging/LoggerTrait.php
Normal file
17
src/Domain/Logging/LoggerTrait.php
Normal file
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Logging;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
trait LoggerTrait
|
||||
{
|
||||
private ?LoggerInterface $logger = null;
|
||||
|
||||
#[Required]
|
||||
public function setLogger(LoggerInterface $logger): void
|
||||
{
|
||||
$this->logger = $logger;
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@
|
||||
namespace App\Domain\Model\Cars;
|
||||
|
||||
use App\Domain\Model\Id\BrandId;
|
||||
use App\Domain\Model\EmbeddingCollection;
|
||||
use App\Domain\Model\Embedding\EmbeddingCollection;
|
||||
|
||||
final readonly class Brand
|
||||
{
|
||||
|
||||
@ -4,6 +4,7 @@ namespace App\Domain\Model\Cars;
|
||||
|
||||
use App\Domain\Model\Id\CarModelId;
|
||||
use App\Domain\Model\Id\CarRevisionId;
|
||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||
use App\Domain\Model\Image;
|
||||
|
||||
final readonly class CarRevision
|
||||
@ -13,5 +14,6 @@ final readonly class CarRevision
|
||||
public readonly CarModelId $carModelId,
|
||||
public readonly string $name,
|
||||
public readonly ?Image $image = null,
|
||||
public readonly EmbeddingIdCollection $embeddings = new EmbeddingIdCollection([]),
|
||||
) {}
|
||||
}
|
||||
@ -1,8 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Model;
|
||||
namespace App\Domain\Model\Embedding;
|
||||
|
||||
use App\Domain\Model\Embedding\Embedding;
|
||||
|
||||
class EmbeddingCollection
|
||||
{
|
||||
@ -5,7 +5,7 @@ namespace App\Domain\Repository;
|
||||
use App\Domain\Model\Embedding\Embedding;
|
||||
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
||||
use App\Domain\Model\Embedding\SmallEmbeddingVector;
|
||||
use App\Domain\Model\EmbeddingCollection;
|
||||
use App\Domain\Model\Embedding\EmbeddingCollection;
|
||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||
|
||||
interface EmbeddingRepository
|
||||
|
||||
@ -3,23 +3,26 @@
|
||||
namespace App\Domain\Search;
|
||||
|
||||
use App\Domain\AI\AIClient;
|
||||
use App\Domain\Logging\LoggerTrait;
|
||||
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\Tiles\Acceleration\AccelerationTileBuilder;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
use App\Domain\Search\Tiles\BatteryTileBuilder;
|
||||
use App\Domain\Search\TileBuilder\BrandTileBuilder;
|
||||
use App\Domain\Search\TileBuilder\PriceTileBuilder;
|
||||
use App\Domain\Search\Tiles\PriceTileBuilder;
|
||||
use App\Domain\Search\TileBuilder\SubSectionTileBuilder;
|
||||
use App\Domain\Search\TileBuilder\SectionTileBuilder;
|
||||
use App\Domain\Search\View\ViewProvider;
|
||||
use App\Domain\Search\View\AiViewBuilderProvider;
|
||||
|
||||
final class AiTileEngine implements Engine
|
||||
{
|
||||
use LoggerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly EmbeddingRepository $embeddingRepository,
|
||||
private readonly CarPropertyRepository $carPropertyRepository,
|
||||
@ -27,7 +30,7 @@ final class AiTileEngine implements Engine
|
||||
private readonly CarRevisionRepository $carRevisionRepository,
|
||||
private readonly BrandRepository $brandRepository,
|
||||
private readonly AIClient $aiClient,
|
||||
private readonly ViewProvider $viewProvider,
|
||||
private readonly AiViewBuilderProvider $viewProvider,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -116,6 +119,8 @@ final class AiTileEngine implements Engine
|
||||
throw new \Exception('Invalid JSON response from AI');
|
||||
}
|
||||
|
||||
return $view->build($data);
|
||||
$this->logger?->debug('Build view {view} with data {data}', ['view' => $view, 'data' => $data]);
|
||||
|
||||
return $view->buildView($data);
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Acceleration;
|
||||
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Acceleration;
|
||||
|
||||
use App\Domain\Model\Cars\Brand;
|
||||
use App\Domain\Model\Cars\CarModel;
|
||||
@ -8,7 +8,8 @@ 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;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
use App\Domain\Search\Tiles\Acceleration\AccelerationTile;
|
||||
|
||||
final readonly class AccelerationTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Availability;
|
||||
|
||||
use App\Domain\Model\Value\Date;
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Availability;
|
||||
|
||||
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;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
use App\Domain\Search\Tiles\Availability\AvailabilityTile;
|
||||
|
||||
final readonly class AvailabilityTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Battery;
|
||||
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Battery;
|
||||
|
||||
use App\Domain\Model\Battery\BatteryProperties;
|
||||
use App\Domain\Model\Cars\Brand;
|
||||
@ -9,7 +9,8 @@ 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;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
use App\Domain\Search\Tiles\Battery\BatteryTile;
|
||||
|
||||
final readonly class BatteryTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Brand;
|
||||
|
||||
class BrandTile
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Car;
|
||||
|
||||
use App\Domain\Model\Image;
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles;
|
||||
|
||||
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
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Consumption;
|
||||
|
||||
use App\Domain\Model\Value\Consumption;
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Consumption;
|
||||
|
||||
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;
|
||||
use App\Domain\Search\Tiles\Consumption\ConsumptionTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class ConsumptionTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Drivetrain;
|
||||
|
||||
use App\Domain\Model\Value\Drivetrain;
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Drivetrain;
|
||||
|
||||
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\Drivetrain\DrivetrainTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class DrivetrainTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Power;
|
||||
|
||||
use App\Domain\Model\Value\Power;
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Power;
|
||||
|
||||
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\Power\PowerTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class PowerTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Price;
|
||||
|
||||
use App\Domain\Model\Value\Price;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Price;
|
||||
|
||||
use App\Domain\Model\Cars\Brand;
|
||||
use App\Domain\Model\Cars\CarModel;
|
||||
@ -8,7 +8,8 @@ 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;
|
||||
use App\Domain\Search\Tiles\Price\PriceTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class PriceTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\ProductionPeriod;
|
||||
|
||||
use App\Domain\Model\Value\Date;
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles\ProductionPeriod;
|
||||
|
||||
use App\Domain\Model\Cars\CarProperty;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\Tiles\ProductionPeriod\ProductionPeriodTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class ProductionPeriodTileBuilder implements TileBuilder
|
||||
{
|
||||
public function build(CarProperty $carProperty): ?TileCollection
|
||||
{
|
||||
if (!$carProperty->value instanceof Production) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new TileCollection([new ProductionPeriodTile(
|
||||
$carProperty->value->productionBegin,
|
||||
$carProperty->value->productionEnd,
|
||||
)]);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Range;
|
||||
|
||||
use App\Domain\Model\Value\Range;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\Range;
|
||||
|
||||
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;
|
||||
use App\Domain\Search\Tiles\Range\RangeTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class RangeTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\RealRange;
|
||||
|
||||
final readonly class RealRangeTile
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\Section;
|
||||
|
||||
class SectionTile
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\SubSection;
|
||||
|
||||
class SubSectionTile
|
||||
{
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles;
|
||||
|
||||
use App\Domain\Model\Cars\CarProperty;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||
@ -1,13 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles;
|
||||
|
||||
use App\Domain\Logging\LoggerTrait;
|
||||
use App\Domain\Model\Cars\CarProperty;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||
|
||||
final readonly class TileBuilderProvider
|
||||
final class TileBuilderProvider
|
||||
{
|
||||
/**
|
||||
* @param iterable<TileBuilder> $tileBuilders
|
||||
@ -15,7 +17,10 @@ final readonly class TileBuilderProvider
|
||||
public function __construct(
|
||||
#[AutowireIterator('app.tile_builder')]
|
||||
private iterable $tileBuilders,
|
||||
) {}
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {
|
||||
$this->logger?->debug('TileBuilderProvider initialized: ' . implode(', ', array_map(fn(TileBuilder $tileBuilder) => get_class($tileBuilder), iterator_to_array($this->tileBuilders))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CarProperty<CarPropertyValue> $carProperty
|
||||
@ -29,6 +34,11 @@ final readonly class TileBuilderProvider
|
||||
}
|
||||
}
|
||||
|
||||
throw new \Exception(sprintf('No tile builder found for car property %s of type %s', $carProperty->carPropertyId->value, get_class($carProperty->value)));
|
||||
$this->logger?->warning('No tile builder found for car property {carPropertyId} of type {carPropertyType}', [
|
||||
'carPropertyId' => $carProperty->carPropertyId->value,
|
||||
'carPropertyType' => get_class($carProperty->value),
|
||||
]);
|
||||
|
||||
return new TileCollection([]);
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\Tiles;
|
||||
namespace App\Domain\Search\Tiles\TopSpeed;
|
||||
|
||||
use App\Domain\Model\Value\Speed;
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\TileBuilder;
|
||||
namespace App\Domain\Search\Tiles\TopSpeed;
|
||||
|
||||
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;
|
||||
use App\Domain\Search\Tiles\TopSpeed\TopSpeedTile;
|
||||
use App\Domain\Search\Tiles\TileBuilder;
|
||||
|
||||
final readonly class TopSpeedTileBuilder implements TileBuilder
|
||||
{
|
||||
@ -5,15 +5,15 @@ namespace App\Domain\Search\View;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||
|
||||
#[AutoconfigureTag('app.view')]
|
||||
interface View
|
||||
#[AutoconfigureTag('app.ai_view_builder')]
|
||||
interface AiViewBuilder
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function build(array $data): TileCollection;
|
||||
public function buildView(array $data): TileCollection;
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
@ -2,24 +2,28 @@
|
||||
|
||||
namespace App\Domain\Search\View;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||
|
||||
final readonly class ViewProvider
|
||||
final readonly class AiViewBuilderProvider
|
||||
{
|
||||
/**
|
||||
* @param iterable<View> $views
|
||||
* @param iterable<AiViewBuilder> $views
|
||||
*/
|
||||
public function __construct(
|
||||
#[AutowireIterator('app.view')]
|
||||
#[AutowireIterator('app.ai_view_builder')]
|
||||
private iterable $views,
|
||||
) {}
|
||||
private readonly ?LoggerInterface $logger = null,
|
||||
) {
|
||||
$this->logger?->debug('AiViewBuilderProvider initialized: ' . implode(', ', array_map(fn(AiViewBuilder $view) => get_class($view), iterator_to_array($this->views))));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $viewClass
|
||||
*
|
||||
* @return View
|
||||
* @return AiViewBuilder
|
||||
*/
|
||||
public function getView(string $viewClass): View
|
||||
public function getView(string $viewClass): AiViewBuilder
|
||||
{
|
||||
foreach ($this->views as $view) {
|
||||
$reflectionClass = new \ReflectionClass($view);
|
||||
@ -34,7 +38,7 @@ final readonly class ViewProvider
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<View>
|
||||
* @return array<AiViewBuilder>
|
||||
*/
|
||||
public function getAllViews(): array
|
||||
{
|
||||
@ -1,31 +0,0 @@
|
||||
<?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.';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\CarRevisionComparison;
|
||||
|
||||
use App\Domain\Model\Id\CarRevisionId;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\View\AiViewBuilder;
|
||||
|
||||
final readonly class CarRevisionComparisonAiViewBuilder implements AiViewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CarRevisionComparisonView $carRevisionComparisonView
|
||||
) {}
|
||||
|
||||
public function buildView(array $data): TileCollection
|
||||
{
|
||||
if (!is_string($data['car_revision_id_1'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Car revision ID 1 is required');
|
||||
}
|
||||
|
||||
if (!is_string($data['car_revision_id_2'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Car revision ID 2 is required');
|
||||
}
|
||||
|
||||
return $this->carRevisionComparisonView->buildView(new CarRevisionId($data['car_revision_id_1']), new CarRevisionId($data['car_revision_id_2']));
|
||||
}
|
||||
|
||||
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.';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\CarRevisionComparison;
|
||||
|
||||
use App\Domain\Model\Id\CarRevisionId;
|
||||
use App\Domain\Search\TileCollection;
|
||||
|
||||
final readonly class CarRevisionComparisonView
|
||||
{
|
||||
public function buildView(CarRevisionId $carRevisionId1, CarRevisionId $carRevisionId2): TileCollection
|
||||
{
|
||||
return new TileCollection([]);
|
||||
}
|
||||
}
|
||||
@ -1,19 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View;
|
||||
namespace App\Domain\Search\View\FullBrand;
|
||||
|
||||
use App\Domain\Model\Id\BrandId;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\View\AiViewBuilder;
|
||||
|
||||
final readonly class FullBrandView implements View
|
||||
final readonly class FullBrandAiViewBuilder implements AiViewBuilder
|
||||
{
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function build(array $data): TileCollection
|
||||
public function __construct(
|
||||
private readonly FullBrandView $fullBrandView
|
||||
) {}
|
||||
|
||||
public function buildView(array $data): TileCollection
|
||||
{
|
||||
return new TileCollection([]);
|
||||
if (!is_string($data['brand_id'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Brand ID is required');
|
||||
}
|
||||
|
||||
return $this->fullBrandView->buildView(new BrandId($data['brand_id']));
|
||||
}
|
||||
|
||||
public function dataDescription(): array
|
||||
14
src/Domain/Search/View/FullBrand/FullBrandView.php
Normal file
14
src/Domain/Search/View/FullBrand/FullBrandView.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\FullBrand;
|
||||
|
||||
use App\Domain\Model\Id\BrandId;
|
||||
use App\Domain\Search\TileCollection;
|
||||
|
||||
final readonly class FullBrandView
|
||||
{
|
||||
public function buildView(BrandId $brandId): TileCollection
|
||||
{
|
||||
return new TileCollection([]);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\FullCarModel;
|
||||
|
||||
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\Tiles\TileBuilderProvider;
|
||||
use App\Domain\Search\Tiles\CarTile;
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
use App\Domain\Search\Tiles\SubSectionTile;
|
||||
use App\Domain\Search\View\AiViewBuilder;
|
||||
|
||||
final readonly class FullCarModelAiViewBuilder implements AiViewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FullCarModelView $fullCarModelView
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function buildView(array $data): TileCollection
|
||||
{
|
||||
if (!is_string($data['car_model_id'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Car model ID is required');
|
||||
}
|
||||
|
||||
return $this->fullCarModelView->buildView(new CarModelId($data['car_model_id']));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View;
|
||||
namespace App\Domain\Search\View\FullCarModel;
|
||||
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||
@ -9,35 +9,25 @@ 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;
|
||||
use App\Domain\Search\Tiles\Car\CarTile;
|
||||
use App\Domain\Search\Tiles\TileBuilderProvider;
|
||||
use App\Domain\Search\Tiles\Section\SectionTile;
|
||||
use App\Domain\Search\Tiles\SubSection\SubSectionTile;
|
||||
|
||||
final readonly class FullCarModelView implements View
|
||||
/** @package App\Domain\Search\View\FullCarModel */
|
||||
final readonly class FullCarModelView
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FullCarLoader $fullCarLoader,
|
||||
private readonly TileBuilderProvider $tileBuilderProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function build(array $data): TileCollection
|
||||
public function buildView(CarModelId $carModelId): TileCollection
|
||||
{
|
||||
if (!is_string($data['car_model_id'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Car model ID is required');
|
||||
}
|
||||
$fullCarModel = $this->fullCarLoader->loadModel($carModelId);
|
||||
|
||||
$fullCarModel = $this->fullCarLoader->loadModel(new CarModelId($data['car_model_id']));
|
||||
$carModel = $fullCarModel->getCarModel();
|
||||
$brand = $fullCarModel->getBrand();
|
||||
$carRevisions = $fullCarModel->getCarRevisions();
|
||||
@ -75,19 +65,4 @@ final readonly class FullCarModelView implements View
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\FullCarRevision;
|
||||
|
||||
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\Cars\CarPropertyValues\V1\AverageConsumption;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryCapacity;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\MotorPower;
|
||||
use App\Domain\Model\Id\CarRevisionId;
|
||||
use App\Domain\Repository\Loader\FullCarLoader;
|
||||
use App\Domain\Search\Tiles\TileBuilderProvider;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\Tiles\CarTile;
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
use App\Domain\Search\View\AiViewBuilder;
|
||||
|
||||
final readonly class FullCarRevisionAiBuilder implements AiViewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FullCarRevisionView $fullCarRevisionView
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function buildView(array $data): TileCollection
|
||||
{
|
||||
if (!is_string($data['car_revision_id'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Car revision ID is required');
|
||||
}
|
||||
|
||||
return $this->fullCarRevisionView->buildView(new CarRevisionId($data['car_revision_id']));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View;
|
||||
namespace App\Domain\Search\View\FullCarRevision;
|
||||
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||
@ -8,33 +8,26 @@ 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\Cars\CarPropertyValues\V1\AverageConsumption;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Battery\BatteryCapacity;
|
||||
use App\Domain\Model\Cars\CarPropertyValues\V1\MotorPower;
|
||||
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\Tiles\TileBuilderProvider;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\Tiles\CarTile;
|
||||
use App\Domain\Search\Tiles\SectionTile;
|
||||
use App\Domain\Search\Tiles\Car\CarTile;
|
||||
use App\Domain\Search\Tiles\Section\SectionTile;
|
||||
|
||||
final readonly class FullCarRevisionView implements View
|
||||
final readonly class FullCarRevisionView
|
||||
{
|
||||
public function __construct(
|
||||
private readonly FullCarLoader $fullCarLoader,
|
||||
private readonly TileBuilderProvider $tileBuilderProvider
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function build(array $data): TileCollection
|
||||
public function buildView(CarRevisionId $carRevisionId): 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']));
|
||||
$fullCar = $this->fullCarLoader->loadRevision($carRevisionId);
|
||||
|
||||
$carRevision = $fullCar->getCarRevision();
|
||||
$carModel = $fullCar->getCarModel();
|
||||
@ -47,6 +40,9 @@ final readonly class FullCarRevisionView implements View
|
||||
$carProperties->getOne(TopSpeed::class),
|
||||
$carProperties->getOne(Acceleration::class),
|
||||
$carProperties->getOne(RangeSpecification::class),
|
||||
$carProperties->getOne(MotorPower::class),
|
||||
$carProperties->getOne(AverageConsumption::class),
|
||||
$carProperties->getOne(BatteryCapacity::class),
|
||||
], static fn($value) => $value !== null);
|
||||
|
||||
$tiles = new TileCollection([]);
|
||||
@ -63,19 +59,4 @@ final readonly class FullCarRevisionView implements View
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\SpecificCarProperty;
|
||||
|
||||
use App\Domain\Model\Id\CarPropertyId;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\View\AiViewBuilder;
|
||||
|
||||
final readonly class SpecificCarPropertyAiViewBuilder implements AiViewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SpecificCarPropertyView $specificCarPropertyView
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $data
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function buildView(array $data): TileCollection
|
||||
{
|
||||
if (!is_array($data['properties'] ?? null)) {
|
||||
throw new \InvalidArgumentException('Properties must be an array');
|
||||
}
|
||||
|
||||
/** @var array<CarPropertyId|null> $propertyIds */
|
||||
$propertyIds = array_map(fn($propertyId) => is_string($propertyId) ? new CarPropertyId($propertyId) : null, $data['properties']);
|
||||
$propertyIds = array_filter($propertyIds, static fn($propertyId) => $propertyId !== null);
|
||||
|
||||
return $this->specificCarPropertyView->buildView($propertyIds);
|
||||
}
|
||||
|
||||
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.';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domain\Search\View\SpecificCarProperty;
|
||||
|
||||
use App\Domain\Model\Id\CarPropertyId;
|
||||
use App\Domain\Repository\CarPropertyRepository;
|
||||
use App\Domain\Repository\Loader\FullCarLoader;
|
||||
use App\Domain\Search\Tiles\TileBuilderProvider;
|
||||
use App\Domain\Search\TileCollection;
|
||||
use App\Domain\Search\Tiles\Section\SectionTile;
|
||||
|
||||
final readonly class SpecificCarPropertyView
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TileBuilderProvider $tileBuilderProvider,
|
||||
private readonly FullCarLoader $fullCarLoader,
|
||||
private readonly CarPropertyRepository $carPropertyRepository,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param array<CarPropertyId> $carPropertyIds
|
||||
*
|
||||
* @return TileCollection
|
||||
*/
|
||||
public function buildView(array $carPropertyIds): TileCollection
|
||||
{
|
||||
$tiles = [];
|
||||
foreach ($carPropertyIds as $carPropertyId) {
|
||||
$carProperty = $this->carPropertyRepository->findById($carPropertyId);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
<?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.';
|
||||
}
|
||||
}
|
||||
@ -2,18 +2,21 @@
|
||||
|
||||
namespace App\Infrastructure\PostgreSQL\Repository\EmbeddingRepository;
|
||||
|
||||
use App\Domain\Logging\LoggerTrait;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use App\Domain\Repository\EmbeddingRepository;
|
||||
use App\Domain\Model\Embedding\Embedding;
|
||||
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
||||
use App\Domain\Model\Embedding\SmallEmbeddingVector;
|
||||
use App\Domain\Model\EmbeddingCollection;
|
||||
use App\Domain\Model\Embedding\EmbeddingCollection;
|
||||
use App\Domain\Model\Value\Vector;
|
||||
use App\Domain\Model\Id\EmbeddingId;
|
||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||
|
||||
final class SqlEmbeddingRepository implements EmbeddingRepository
|
||||
{
|
||||
use LoggerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
) {}
|
||||
@ -66,29 +69,33 @@ final class SqlEmbeddingRepository implements EmbeddingRepository
|
||||
return $this->mapRowToEmbedding($row);
|
||||
}
|
||||
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 20): EmbeddingCollection
|
||||
public function searchByLargeEmbeddingVector(LargeEmbeddingVector $embeddingVector, int $limit = 99): EmbeddingCollection
|
||||
{
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT *, large_embedding_vector <=> :embeddingVector AS distance
|
||||
FROM embeddings
|
||||
WHERE large_embedding_vector IS NOT NULL
|
||||
ORDER BY large_embedding_vector <=> :embeddingVector
|
||||
LIMIT :limit',
|
||||
ORDER BY large_embedding_vector <=> :embeddingVector',
|
||||
[
|
||||
'embeddingVector' => '[' . implode(',', $embeddingVector->vector->values) . ']',
|
||||
'limit' => $limit,
|
||||
]
|
||||
);
|
||||
|
||||
$embeddings = [];
|
||||
foreach ($result->fetchAllAssociative() as $row) {
|
||||
if ($row['distance'] > 0.5) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->logger?->debug('Found embedding {embedding} with distance {distance}', ['embedding' => $row['phrase'], 'distance' => $row['distance']]);
|
||||
|
||||
$embeddings[] = $this->mapRowToEmbedding($row);
|
||||
}
|
||||
|
||||
return new EmbeddingCollection($embeddings);
|
||||
}
|
||||
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 20): EmbeddingCollection
|
||||
public function searchBySmallEmbeddingVector(SmallEmbeddingVector $smallEmbeddingVector, int $limit = 99): EmbeddingCollection
|
||||
{
|
||||
$result = $this->connection->executeQuery(
|
||||
'SELECT *
|
||||
|
||||
12
symfony.lock
12
symfony.lock
@ -136,6 +136,18 @@
|
||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
||||
}
|
||||
},
|
||||
"symfony/monolog-bundle": {
|
||||
"version": "3.10",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.7",
|
||||
"ref": "aff23899c4440dd995907613c1dd709b6f59503f"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/monolog.yaml"
|
||||
]
|
||||
},
|
||||
"symfony/routing": {
|
||||
"version": "7.2",
|
||||
"recipe": {
|
||||
|
||||
3
templates/_components/result.html.twig
Normal file
3
templates/_components/result.html.twig
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="tiles-grid">
|
||||
{% include 'result/tiles/collection.html.twig' with { tiles: tiles } %}
|
||||
</div>
|
||||
@ -1,7 +1,8 @@
|
||||
<div class="search-container" id="searchForm">
|
||||
<input type="text" id="searchInput" class="search-input" placeholder="🔍 Search for electric vehicles, brands, or models..." value="{{ query|default('') }}">
|
||||
<button type="button" id="searchButton" class="search-button">
|
||||
<i class="fas fa-search"></i> Search
|
||||
<span class="search-btn-content"><i class="fas fa-search"></i> Search</span>
|
||||
<span class="search-btn-spinner" style="display:none;"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -9,39 +10,380 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchForm = document.getElementById('searchForm');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchButton = document.getElementById('searchButton');
|
||||
const resultsContainer = document.getElementById('resultsContainer');
|
||||
|
||||
function encodeSearchQuery(query) {
|
||||
query = query.replace(/[^a-zA-Z0-9+\-\s]/g, '');
|
||||
return encodeURIComponent(query);
|
||||
}
|
||||
|
||||
function setLoading(isLoading) {
|
||||
if (isLoading) {
|
||||
searchForm.classList.add('loading');
|
||||
searchButton.querySelector('.search-btn-content').style.display = 'none';
|
||||
searchButton.querySelector('.search-btn-spinner').style.display = 'inline-block';
|
||||
|
||||
if (resultsContainer) {
|
||||
if (resultsContainer.innerHTML.trim()) {
|
||||
// If there is already content, just fade it
|
||||
resultsContainer.classList.add('loading-fade');
|
||||
} else {
|
||||
// If no content, show placeholders
|
||||
fadeOutIn(resultsContainer, `
|
||||
<div class="placeholder-glow placeholder-detail-layout">
|
||||
<div class="placeholder-header">
|
||||
<div class="placeholder placeholder-title-main"></div>
|
||||
<div class="placeholder placeholder-title-sub"></div>
|
||||
</div>
|
||||
<div class="placeholder-content-row">
|
||||
<div class="placeholder-image-col">
|
||||
<div class="placeholder placeholder-large-img"></div>
|
||||
</div>
|
||||
<div class="placeholder-info-col">
|
||||
<div class="placeholder-info-grid">
|
||||
<div class="placeholder-info-block">
|
||||
<div class="placeholder placeholder-icon"></div>
|
||||
<div class="placeholder placeholder-value"></div>
|
||||
<div class="placeholder placeholder-label"></div>
|
||||
</div>
|
||||
<div class="placeholder-info-block">
|
||||
<div class="placeholder placeholder-icon"></div>
|
||||
<div class="placeholder placeholder-value"></div>
|
||||
<div class="placeholder placeholder-label"></div>
|
||||
</div>
|
||||
<div class="placeholder-info-block">
|
||||
<div class="placeholder placeholder-icon"></div>
|
||||
<div class="placeholder placeholder-value"></div>
|
||||
<div class="placeholder placeholder-label"></div>
|
||||
</div>
|
||||
<div class="placeholder-info-block">
|
||||
<div class="placeholder placeholder-icon"></div>
|
||||
<div class="placeholder placeholder-value"></div>
|
||||
<div class="placeholder placeholder-label"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`, () => {
|
||||
resultsContainer.style.display = 'block';
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
searchForm.classList.remove('loading');
|
||||
// Hide spinner, show button content
|
||||
searchButton.querySelector('.search-btn-content').style.display = 'inline-block';
|
||||
searchButton.querySelector('.search-btn-spinner').style.display = 'none';
|
||||
if (resultsContainer) {
|
||||
resultsContainer.classList.remove('loading-fade');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fadeOutIn(element, newContent, callback) {
|
||||
element.classList.add('fade-out');
|
||||
setTimeout(() => {
|
||||
element.innerHTML = newContent;
|
||||
element.classList.remove('fade-out');
|
||||
element.classList.add('fade-in');
|
||||
setTimeout(() => {
|
||||
element.classList.remove('fade-in');
|
||||
if (callback) callback();
|
||||
}, 150); // match the CSS transition duration
|
||||
}, 150); // match the CSS transition duration
|
||||
}
|
||||
|
||||
function performSearch(query) {
|
||||
if (!query.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const encodedQuery = encodeSearchQuery(query);
|
||||
|
||||
// Show loading animation on search bar
|
||||
setLoading(true);
|
||||
|
||||
// Update URL without page reload
|
||||
const newUrl = `/s/${encodedQuery}`;
|
||||
window.history.pushState({ query: query }, '', newUrl);
|
||||
|
||||
// Make AJAX request to get results
|
||||
fetch(`/result/${encodedQuery}`)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.text();
|
||||
})
|
||||
.then(html => {
|
||||
// Hide loading animation
|
||||
setLoading(false);
|
||||
|
||||
fadeOutIn(resultsContainer, html, () => {
|
||||
resultsContainer.style.display = 'block';
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Search error:', error);
|
||||
|
||||
// Hide loading animation
|
||||
setLoading(false);
|
||||
|
||||
// Show error message
|
||||
fadeOutIn(resultsContainer, '<div class="no-results">Sorry, there was an error performing your search. Please try again.</div>', () => {
|
||||
resultsContainer.style.display = 'block';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
searchForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
const encodedQuery = encodeSearchQuery(query);
|
||||
window.location.href = `/s/${encodedQuery}`;
|
||||
}
|
||||
performSearch(query);
|
||||
});
|
||||
|
||||
// Handle search button click
|
||||
searchButton.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
const encodedQuery = encodeSearchQuery(query);
|
||||
window.location.href = `/s/${encodedQuery}`;
|
||||
}
|
||||
performSearch(query);
|
||||
});
|
||||
|
||||
// Handle Enter key press
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const query = searchInput.value.trim();
|
||||
if (query) {
|
||||
const encodedQuery = encodeSearchQuery(query);
|
||||
window.location.href = `/s/${encodedQuery}`;
|
||||
}
|
||||
performSearch(query);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle browser back/forward buttons
|
||||
window.addEventListener('popstate', function(event) {
|
||||
if (event.state && event.state.query) {
|
||||
searchInput.value = event.state.query;
|
||||
performSearch(event.state.query);
|
||||
}
|
||||
});
|
||||
|
||||
// Check for initial results on the page
|
||||
const initialResults = document.getElementById('initialResults');
|
||||
const initialQuery = searchInput.value.trim();
|
||||
|
||||
if (initialResults && initialResults.innerHTML.trim()) {
|
||||
// Show initial results if they exist
|
||||
resultsContainer.innerHTML = initialResults.innerHTML;
|
||||
resultsContainer.style.display = 'block';
|
||||
} else if (initialQuery) {
|
||||
// If there's a query but no initial results, perform search
|
||||
performSearch(initialQuery);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.search-container.loading {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.search-btn-spinner {
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
border: 2.5px solid #e0e0e0;
|
||||
border-top: 2.5px solid #083d77;
|
||||
border-radius: 50%;
|
||||
animation: search-btn-spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.2em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes search-btn-spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.placeholder-glow {
|
||||
display: block;
|
||||
animation: placeholder-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes placeholder-glow {
|
||||
0% { opacity: 0.5; }
|
||||
50% { opacity: 1; }
|
||||
100% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: block;
|
||||
background-color:rgb(198, 198, 198);
|
||||
height: 1.2rem;
|
||||
width: 100%;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.2em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.placeholder.col-6 {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
.placeholder.col-7 {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.placeholder.col-4 {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.placeholder.col-8 {
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.placeholder-tiles {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
justify-content: stretch;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.placeholder-tile {
|
||||
background: #f6f6f6;
|
||||
padding: 1.5rem 1.2rem 1.2rem 1.2rem;
|
||||
width: 260px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
min-height: 320px;
|
||||
}
|
||||
.placeholder-img {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
background-color: #d2d2d2;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.placeholder-title {
|
||||
width: 70%;
|
||||
height: 1.3rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.placeholder-subtitle {
|
||||
width: 50%;
|
||||
height: 1rem;
|
||||
margin-bottom: 1.1rem;
|
||||
}
|
||||
.placeholder-spec {
|
||||
width: 80%;
|
||||
height: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.placeholder-spec.short {
|
||||
width: 40%;
|
||||
}
|
||||
.placeholder-btn {
|
||||
width: 60%;
|
||||
height: 1.2rem;
|
||||
border-radius: 0.6rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.placeholder-detail-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2.5rem;
|
||||
margin-top: 2rem;
|
||||
width: 100%;
|
||||
}
|
||||
.placeholder-header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.placeholder-title-main {
|
||||
width: 320px;
|
||||
height: 2.2rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.placeholder-title-sub {
|
||||
width: 220px;
|
||||
height: 1.2rem;
|
||||
margin-bottom: 0.7rem;
|
||||
}
|
||||
.placeholder-content-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.placeholder-image-col {
|
||||
flex: 1.2;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.placeholder-large-img {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
height: 260px;
|
||||
border-radius: 0.7rem;
|
||||
background-color: #d2d2d2;
|
||||
}
|
||||
.placeholder-info-col {
|
||||
flex: 1.5;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.placeholder-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
gap: 2.2rem 2.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.placeholder-info-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.placeholder-icon {
|
||||
width: 2.2rem;
|
||||
height: 2.2rem;
|
||||
border-radius: 50%;
|
||||
background-color: #e0e0e0;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.placeholder-value {
|
||||
width: 90px;
|
||||
height: 1.3rem;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.placeholder-label {
|
||||
width: 120px;
|
||||
height: 0.9rem;
|
||||
background-color: #eaeaea;
|
||||
}
|
||||
#resultsContainer {
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
#resultsContainer.fade-out {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
#resultsContainer.fade-in {
|
||||
opacity: 1;
|
||||
}
|
||||
/* Add fade for loading existing content */
|
||||
#resultsContainer.loading-fade {
|
||||
opacity: 0.3;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
</style>
|
||||
@ -263,7 +263,6 @@
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
@ -8,4 +8,5 @@
|
||||
|
||||
{% include '_components/search.html.twig' %}
|
||||
</div>
|
||||
<div id="resultsContainer" class="results-container"></div>
|
||||
{% endblock %}
|
||||
@ -6,8 +6,10 @@
|
||||
|
||||
{% include '_components/search.html.twig' with { query: query } %}
|
||||
</div>
|
||||
|
||||
<div class="tiles-grid">
|
||||
{% include 'result/tiles/collection.html.twig' with { tiles: tiles } %}
|
||||
</div>
|
||||
|
||||
{% if tiles is defined and tiles|length > 0 %}
|
||||
<div id="resultsContainer" class="results-container">
|
||||
{% include '_components/result.html.twig' with { tiles: tiles } %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user