refactored
This commit is contained in:
parent
3f78e2e9f1
commit
7051bbf5b7
@ -19,6 +19,7 @@
|
|||||||
"symfony/dotenv": "7.2.*",
|
"symfony/dotenv": "7.2.*",
|
||||||
"symfony/flex": "^2",
|
"symfony/flex": "^2",
|
||||||
"symfony/framework-bundle": "7.2.*",
|
"symfony/framework-bundle": "7.2.*",
|
||||||
|
"symfony/monolog-bundle": "^3.10",
|
||||||
"symfony/runtime": "7.2.*",
|
"symfony/runtime": "7.2.*",
|
||||||
"symfony/serializer": "7.2.*",
|
"symfony/serializer": "7.2.*",
|
||||||
"symfony/twig-bundle": "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",
|
"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": "e4da23c3811aae55314b0e018026605a",
|
"content-hash": "45fe64d8d59b093a4dc302486a3a607d",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "composer/semver",
|
"name": "composer/semver",
|
||||||
@ -792,6 +792,109 @@
|
|||||||
},
|
},
|
||||||
"time": "2025-01-24T11:45:48+00:00"
|
"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",
|
"name": "nyholm/psr7",
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
@ -3157,6 +3260,165 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-05-29T07:47:32+00:00"
|
"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",
|
"name": "symfony/polyfill-ctype",
|
||||||
"version": "v1.32.0",
|
"version": "v1.32.0",
|
||||||
@ -4572,91 +4834,6 @@
|
|||||||
],
|
],
|
||||||
"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",
|
||||||
@ -5314,11 +5491,96 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2025-04-17T09:11:12+00:00"
|
"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": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
@ -5328,6 +5590,6 @@
|
|||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-pgsql": "*"
|
"ext-pgsql": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,4 +8,5 @@ return [
|
|||||||
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],
|
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;
|
namespace App\Application\Commands;
|
||||||
|
|
||||||
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
use App\Domain\ContentManagement\CarPropertyEmbedder;
|
||||||
|
use App\Domain\ContentManagement\CarRevisionEmbedder;
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
@ -62,6 +63,7 @@ class LoadFixtures extends Command
|
|||||||
private readonly CarRevisionRepository $carRevisionRepository,
|
private readonly CarRevisionRepository $carRevisionRepository,
|
||||||
private readonly CarPropertyRepository $carPropertyRepository,
|
private readonly CarPropertyRepository $carPropertyRepository,
|
||||||
private readonly CarPropertyEmbedder $carPropertyEmbedder,
|
private readonly CarPropertyEmbedder $carPropertyEmbedder,
|
||||||
|
private readonly CarRevisionEmbedder $carRevisionEmbedder,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@ -154,6 +156,7 @@ class LoadFixtures extends Command
|
|||||||
$image
|
$image
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->carRevisionEmbedder->createPersistedEmbedding($carRevision, $carModel, $brand);
|
||||||
$this->carRevisionRepository->save($carRevision);
|
$this->carRevisionRepository->save($carRevision);
|
||||||
|
|
||||||
foreach ($revisionFixture['properties'] as $propertyValue) {
|
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 RangeSpecification(new NefzRange(new Range(614)), new WltpRange(new Range(602))),
|
||||||
new CatalogPrice(new Price(129990, Currency::euro())),
|
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')]
|
#[Route('/s/{query}', name: 'search')]
|
||||||
public function index(string $query): Response
|
public function index(string $query): Response
|
||||||
{
|
{
|
||||||
$decodedQuery = urldecode(str_replace('+', ' ', $query));
|
$decodedQuery = urldecode($query);
|
||||||
|
|
||||||
return $this->render('result/index.html.twig', [
|
return $this->render('result/index.html.twig', [
|
||||||
'tiles' => $this->engine->search($decodedQuery)->array(),
|
'tiles' => $this->engine->search($decodedQuery)->array(),
|
||||||
'query' => $decodedQuery,
|
'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
|
public function generateJson(string $prompt): array
|
||||||
{
|
{
|
||||||
$response = $this->client->chat()->create([
|
$response = $this->client->chat()->create([
|
||||||
'model' => 'gpt-4.1-nano',
|
'model' => 'gpt-4.1-mini',
|
||||||
'messages' => [
|
'messages' => [
|
||||||
['role' => 'user', 'content' => $prompt],
|
['role' => 'user', 'content' => $prompt],
|
||||||
],
|
],
|
||||||
|
|||||||
@ -12,6 +12,7 @@ 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;
|
||||||
|
|
||||||
|
|
||||||
class CarPropertyEmbedder
|
class CarPropertyEmbedder
|
||||||
{
|
{
|
||||||
public function __construct(
|
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
|
public function createPersistedEmbedding(CarProperty $carProperty, ?CarRevision $carRevision, ?CarModel $carModel, ?Brand $brand): ?Embedding
|
||||||
{
|
{
|
||||||
$text = $carProperty->value->humanReadable();
|
$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;
|
namespace App\Domain\Model\Cars;
|
||||||
|
|
||||||
use App\Domain\Model\Id\BrandId;
|
use App\Domain\Model\Id\BrandId;
|
||||||
use App\Domain\Model\EmbeddingCollection;
|
use App\Domain\Model\Embedding\EmbeddingCollection;
|
||||||
|
|
||||||
final readonly class Brand
|
final readonly class Brand
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,6 +4,7 @@ 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;
|
||||||
|
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||||
use App\Domain\Model\Image;
|
use App\Domain\Model\Image;
|
||||||
|
|
||||||
final readonly class CarRevision
|
final readonly class CarRevision
|
||||||
@ -13,5 +14,6 @@ final readonly class CarRevision
|
|||||||
public readonly CarModelId $carModelId,
|
public readonly CarModelId $carModelId,
|
||||||
public readonly string $name,
|
public readonly string $name,
|
||||||
public readonly ?Image $image = null,
|
public readonly ?Image $image = null,
|
||||||
|
public readonly EmbeddingIdCollection $embeddings = new EmbeddingIdCollection([]),
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Model;
|
namespace App\Domain\Model\Embedding;
|
||||||
|
|
||||||
use App\Domain\Model\Embedding\Embedding;
|
|
||||||
|
|
||||||
class EmbeddingCollection
|
class EmbeddingCollection
|
||||||
{
|
{
|
||||||
@ -5,7 +5,7 @@ namespace App\Domain\Repository;
|
|||||||
use App\Domain\Model\Embedding\Embedding;
|
use App\Domain\Model\Embedding\Embedding;
|
||||||
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
||||||
use App\Domain\Model\Embedding\SmallEmbeddingVector;
|
use App\Domain\Model\Embedding\SmallEmbeddingVector;
|
||||||
use App\Domain\Model\EmbeddingCollection;
|
use App\Domain\Model\Embedding\EmbeddingCollection;
|
||||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||||
|
|
||||||
interface EmbeddingRepository
|
interface EmbeddingRepository
|
||||||
|
|||||||
@ -3,23 +3,26 @@
|
|||||||
namespace App\Domain\Search;
|
namespace App\Domain\Search;
|
||||||
|
|
||||||
use App\Domain\AI\AIClient;
|
use App\Domain\AI\AIClient;
|
||||||
|
use App\Domain\Logging\LoggerTrait;
|
||||||
use App\Domain\Model\Embedding\Embedding;
|
use App\Domain\Model\Embedding\Embedding;
|
||||||
use App\Domain\Repository\BrandRepository;
|
use App\Domain\Repository\BrandRepository;
|
||||||
use App\Domain\Repository\CarModelRepository;
|
use App\Domain\Repository\CarModelRepository;
|
||||||
use App\Domain\Repository\CarPropertyRepository;
|
use App\Domain\Repository\CarPropertyRepository;
|
||||||
use App\Domain\Repository\CarRevisionRepository;
|
use App\Domain\Repository\CarRevisionRepository;
|
||||||
use App\Domain\Repository\EmbeddingRepository;
|
use App\Domain\Repository\EmbeddingRepository;
|
||||||
use App\Domain\Search\TileBuilder\AccelerationTileBuilder;
|
use App\Domain\Search\Tiles\Acceleration\AccelerationTileBuilder;
|
||||||
use App\Domain\Search\TileBuilder\TileBuilder;
|
use App\Domain\Search\Tiles\TileBuilder;
|
||||||
use App\Domain\Search\TileBuilder\BatteryTileBuilder;
|
use App\Domain\Search\Tiles\BatteryTileBuilder;
|
||||||
use App\Domain\Search\TileBuilder\BrandTileBuilder;
|
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\SubSectionTileBuilder;
|
||||||
use App\Domain\Search\TileBuilder\SectionTileBuilder;
|
use App\Domain\Search\TileBuilder\SectionTileBuilder;
|
||||||
use App\Domain\Search\View\ViewProvider;
|
use App\Domain\Search\View\AiViewBuilderProvider;
|
||||||
|
|
||||||
final class AiTileEngine implements Engine
|
final class AiTileEngine implements Engine
|
||||||
{
|
{
|
||||||
|
use LoggerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly EmbeddingRepository $embeddingRepository,
|
private readonly EmbeddingRepository $embeddingRepository,
|
||||||
private readonly CarPropertyRepository $carPropertyRepository,
|
private readonly CarPropertyRepository $carPropertyRepository,
|
||||||
@ -27,7 +30,7 @@ final class AiTileEngine implements Engine
|
|||||||
private readonly CarRevisionRepository $carRevisionRepository,
|
private readonly CarRevisionRepository $carRevisionRepository,
|
||||||
private readonly BrandRepository $brandRepository,
|
private readonly BrandRepository $brandRepository,
|
||||||
private readonly AIClient $aiClient,
|
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');
|
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
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Acceleration;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Acceleration;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
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\CarRevision;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Acceleration;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class AccelerationTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Availability;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Date;
|
use App\Domain\Model\Value\Date;
|
||||||
|
|
||||||
@ -1,14 +1,12 @@
|
|||||||
<?php
|
<?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\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class AvailabilityTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Battery;
|
||||||
|
|
||||||
use App\Domain\Model\Battery\BatteryProperties;
|
use App\Domain\Model\Battery\BatteryProperties;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Battery;
|
||||||
|
|
||||||
use App\Domain\Model\Battery\BatteryProperties;
|
use App\Domain\Model\Battery\BatteryProperties;
|
||||||
use App\Domain\Model\Cars\Brand;
|
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\CarPropertyValues\V1\Battery\BatteryType;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class BatteryTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Brand;
|
||||||
|
|
||||||
class BrandTile
|
class BrandTile
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Car;
|
||||||
|
|
||||||
use App\Domain\Model\Image;
|
use App\Domain\Model\Image;
|
||||||
|
|
||||||
@ -1,13 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargeTimeProperties;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\Charging\ChargeTimeProperties;
|
||||||
use App\Domain\Search\Tiles\ChargingTile;
|
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
|
|
||||||
final readonly class ChargingTileBuilder implements TileBuilder
|
final readonly class ChargingTileBuilder implements TileBuilder
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Consumption;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Consumption;
|
use App\Domain\Model\Value\Consumption;
|
||||||
|
|
||||||
@ -1,13 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Consumption;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class ConsumptionTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Drivetrain;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Drivetrain;
|
use App\Domain\Model\Value\Drivetrain;
|
||||||
|
|
||||||
@ -1,12 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Drivetrain;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\Drivetrain\DrivetrainTile;
|
||||||
|
use App\Domain\Search\Tiles\TileBuilder;
|
||||||
|
|
||||||
final readonly class DrivetrainTileBuilder implements TileBuilder
|
final readonly class DrivetrainTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Power;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Power;
|
use App\Domain\Model\Value\Power;
|
||||||
|
|
||||||
@ -1,12 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Power;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
use App\Domain\Model\Cars\CarModel;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarRevision;
|
use App\Domain\Model\Cars\CarRevision;
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
|
use App\Domain\Search\Tiles\Power\PowerTile;
|
||||||
|
use App\Domain\Search\Tiles\TileBuilder;
|
||||||
|
|
||||||
final readonly class PowerTileBuilder implements TileBuilder
|
final readonly class PowerTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Price;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Price;
|
use App\Domain\Model\Value\Price;
|
||||||
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Price;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\Brand;
|
use App\Domain\Model\Cars\Brand;
|
||||||
use App\Domain\Model\Cars\CarModel;
|
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\CarRevision;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CatalogPrice;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CatalogPrice;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class PriceTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\ProductionPeriod;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Date;
|
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
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Range;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Range;
|
use App\Domain\Model\Value\Range;
|
||||||
|
|
||||||
@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\Range;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\RangeSpecification;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class RangeTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\RealRange;
|
||||||
|
|
||||||
final readonly class RealRangeTile
|
final readonly class RealRangeTile
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\Section;
|
||||||
|
|
||||||
class SectionTile
|
class SectionTile
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\SubSection;
|
||||||
|
|
||||||
class SubSectionTile
|
class SubSectionTile
|
||||||
{
|
{
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
@ -1,13 +1,15 @@
|
|||||||
<?php
|
<?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\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
||||||
|
|
||||||
final readonly class TileBuilderProvider
|
final class TileBuilderProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param iterable<TileBuilder> $tileBuilders
|
* @param iterable<TileBuilder> $tileBuilders
|
||||||
@ -15,7 +17,10 @@ final readonly class TileBuilderProvider
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
#[AutowireIterator('app.tile_builder')]
|
#[AutowireIterator('app.tile_builder')]
|
||||||
private iterable $tileBuilders,
|
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
|
* @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
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\Tiles;
|
namespace App\Domain\Search\Tiles\TopSpeed;
|
||||||
|
|
||||||
use App\Domain\Model\Value\Speed;
|
use App\Domain\Model\Value\Speed;
|
||||||
|
|
||||||
@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Domain\Search\TileBuilder;
|
namespace App\Domain\Search\Tiles\TopSpeed;
|
||||||
|
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\TopSpeed;
|
||||||
use App\Domain\Search\TileCollection;
|
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
|
final readonly class TopSpeedTileBuilder implements TileBuilder
|
||||||
{
|
{
|
||||||
@ -5,15 +5,15 @@ namespace App\Domain\Search\View;
|
|||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
||||||
|
|
||||||
#[AutoconfigureTag('app.view')]
|
#[AutoconfigureTag('app.ai_view_builder')]
|
||||||
interface View
|
interface AiViewBuilder
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param array<mixed> $data
|
* @param array<mixed> $data
|
||||||
*
|
*
|
||||||
* @return TileCollection
|
* @return TileCollection
|
||||||
*/
|
*/
|
||||||
public function build(array $data): TileCollection;
|
public function buildView(array $data): TileCollection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
@ -2,24 +2,28 @@
|
|||||||
|
|
||||||
namespace App\Domain\Search\View;
|
namespace App\Domain\Search\View;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
|
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(
|
public function __construct(
|
||||||
#[AutowireIterator('app.view')]
|
#[AutowireIterator('app.ai_view_builder')]
|
||||||
private iterable $views,
|
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
|
* @param string $viewClass
|
||||||
*
|
*
|
||||||
* @return View
|
* @return AiViewBuilder
|
||||||
*/
|
*/
|
||||||
public function getView(string $viewClass): View
|
public function getView(string $viewClass): AiViewBuilder
|
||||||
{
|
{
|
||||||
foreach ($this->views as $view) {
|
foreach ($this->views as $view) {
|
||||||
$reflectionClass = new \ReflectionClass($view);
|
$reflectionClass = new \ReflectionClass($view);
|
||||||
@ -34,7 +38,7 @@ final readonly class ViewProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<View>
|
* @return array<AiViewBuilder>
|
||||||
*/
|
*/
|
||||||
public function getAllViews(): array
|
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
|
<?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\TileCollection;
|
||||||
|
use App\Domain\Search\View\AiViewBuilder;
|
||||||
|
|
||||||
final readonly class FullBrandView implements View
|
final readonly class FullBrandAiViewBuilder implements AiViewBuilder
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* @param array<mixed> $data
|
private readonly FullBrandView $fullBrandView
|
||||||
*
|
) {}
|
||||||
* @return TileCollection
|
|
||||||
*/
|
public function buildView(array $data): TileCollection
|
||||||
public function build(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
|
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
|
<?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\Acceleration;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
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\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
use App\Domain\Model\Cars\CarProperty;
|
||||||
use App\Domain\Model\Id\CarModelId;
|
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\FullCarLoader;
|
||||||
use App\Domain\Repository\Loader\FullCarRevisionLoader;
|
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
use App\Domain\Search\TileBuilder\TileBuilderProvider;
|
use App\Domain\Search\Tiles\Car\CarTile;
|
||||||
use App\Domain\Search\Tiles\CarTile;
|
use App\Domain\Search\Tiles\TileBuilderProvider;
|
||||||
use App\Domain\Search\Tiles\SectionTile;
|
use App\Domain\Search\Tiles\Section\SectionTile;
|
||||||
use App\Domain\Search\Tiles\SubSectionTile;
|
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(
|
public function __construct(
|
||||||
private readonly FullCarLoader $fullCarLoader,
|
private readonly FullCarLoader $fullCarLoader,
|
||||||
private readonly TileBuilderProvider $tileBuilderProvider,
|
private readonly TileBuilderProvider $tileBuilderProvider,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
public function buildView(CarModelId $carModelId): TileCollection
|
||||||
* @param array<mixed> $data
|
|
||||||
*
|
|
||||||
* @return TileCollection
|
|
||||||
*/
|
|
||||||
public function build(array $data): TileCollection
|
|
||||||
{
|
{
|
||||||
if (!is_string($data['car_model_id'] ?? null)) {
|
$fullCarModel = $this->fullCarLoader->loadModel($carModelId);
|
||||||
throw new \InvalidArgumentException('Car model ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullCarModel = $this->fullCarLoader->loadModel(new CarModelId($data['car_model_id']));
|
|
||||||
$carModel = $fullCarModel->getCarModel();
|
$carModel = $fullCarModel->getCarModel();
|
||||||
$brand = $fullCarModel->getBrand();
|
$brand = $fullCarModel->getBrand();
|
||||||
$carRevisions = $fullCarModel->getCarRevisions();
|
$carRevisions = $fullCarModel->getCarRevisions();
|
||||||
@ -75,19 +65,4 @@ final readonly class FullCarModelView implements View
|
|||||||
|
|
||||||
return new TileCollection($allTiles);
|
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
|
<?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\Acceleration;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\Production;
|
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\TopSpeed;
|
||||||
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
use App\Domain\Model\Cars\CarPropertyValues\V1\CarPropertyValue;
|
||||||
use App\Domain\Model\Cars\CarProperty;
|
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\Model\Id\CarRevisionId;
|
||||||
use App\Domain\Repository\Loader\FullCarLoader;
|
use App\Domain\Repository\Loader\FullCarLoader;
|
||||||
use App\Domain\Repository\Loader\FullCarRevisionLoader;
|
use App\Domain\Search\Tiles\TileBuilderProvider;
|
||||||
use App\Domain\Search\TileBuilder\TileBuilderProvider;
|
|
||||||
use App\Domain\Search\TileCollection;
|
use App\Domain\Search\TileCollection;
|
||||||
use App\Domain\Search\Tiles\CarTile;
|
use App\Domain\Search\Tiles\Car\CarTile;
|
||||||
use App\Domain\Search\Tiles\SectionTile;
|
use App\Domain\Search\Tiles\Section\SectionTile;
|
||||||
|
|
||||||
final readonly class FullCarRevisionView implements View
|
final readonly class FullCarRevisionView
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly FullCarLoader $fullCarLoader,
|
private readonly FullCarLoader $fullCarLoader,
|
||||||
private readonly TileBuilderProvider $tileBuilderProvider
|
private readonly TileBuilderProvider $tileBuilderProvider
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
public function buildView(CarRevisionId $carRevisionId): TileCollection
|
||||||
* @param array<mixed> $data
|
|
||||||
*
|
|
||||||
* @return TileCollection
|
|
||||||
*/
|
|
||||||
public function build(array $data): TileCollection
|
|
||||||
{
|
{
|
||||||
if (!is_string($data['car_revision_id'] ?? null)) {
|
$fullCar = $this->fullCarLoader->loadRevision($carRevisionId);
|
||||||
throw new \InvalidArgumentException('Car revision ID is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$fullCar = $this->fullCarLoader->loadRevision(new CarRevisionId($data['car_revision_id']));
|
|
||||||
|
|
||||||
$carRevision = $fullCar->getCarRevision();
|
$carRevision = $fullCar->getCarRevision();
|
||||||
$carModel = $fullCar->getCarModel();
|
$carModel = $fullCar->getCarModel();
|
||||||
@ -47,6 +40,9 @@ final readonly class FullCarRevisionView implements View
|
|||||||
$carProperties->getOne(TopSpeed::class),
|
$carProperties->getOne(TopSpeed::class),
|
||||||
$carProperties->getOne(Acceleration::class),
|
$carProperties->getOne(Acceleration::class),
|
||||||
$carProperties->getOne(RangeSpecification::class),
|
$carProperties->getOne(RangeSpecification::class),
|
||||||
|
$carProperties->getOne(MotorPower::class),
|
||||||
|
$carProperties->getOne(AverageConsumption::class),
|
||||||
|
$carProperties->getOne(BatteryCapacity::class),
|
||||||
], static fn($value) => $value !== null);
|
], static fn($value) => $value !== null);
|
||||||
|
|
||||||
$tiles = new TileCollection([]);
|
$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;
|
namespace App\Infrastructure\PostgreSQL\Repository\EmbeddingRepository;
|
||||||
|
|
||||||
|
use App\Domain\Logging\LoggerTrait;
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use App\Domain\Repository\EmbeddingRepository;
|
use App\Domain\Repository\EmbeddingRepository;
|
||||||
use App\Domain\Model\Embedding\Embedding;
|
use App\Domain\Model\Embedding\Embedding;
|
||||||
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
use App\Domain\Model\Embedding\LargeEmbeddingVector;
|
||||||
use App\Domain\Model\Embedding\SmallEmbeddingVector;
|
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\Value\Vector;
|
||||||
use App\Domain\Model\Id\EmbeddingId;
|
use App\Domain\Model\Id\EmbeddingId;
|
||||||
use App\Domain\Model\Id\EmbeddingIdCollection;
|
use App\Domain\Model\Id\EmbeddingIdCollection;
|
||||||
|
|
||||||
final class SqlEmbeddingRepository implements EmbeddingRepository
|
final class SqlEmbeddingRepository implements EmbeddingRepository
|
||||||
{
|
{
|
||||||
|
use LoggerTrait;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Connection $connection,
|
private readonly Connection $connection,
|
||||||
) {}
|
) {}
|
||||||
@ -66,29 +69,33 @@ final class SqlEmbeddingRepository implements EmbeddingRepository
|
|||||||
return $this->mapRowToEmbedding($row);
|
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(
|
$result = $this->connection->executeQuery(
|
||||||
'SELECT *, large_embedding_vector <=> :embeddingVector AS distance
|
'SELECT *, large_embedding_vector <=> :embeddingVector AS distance
|
||||||
FROM embeddings
|
FROM embeddings
|
||||||
WHERE large_embedding_vector IS NOT NULL
|
WHERE large_embedding_vector IS NOT NULL
|
||||||
ORDER BY large_embedding_vector <=> :embeddingVector
|
ORDER BY large_embedding_vector <=> :embeddingVector',
|
||||||
LIMIT :limit',
|
|
||||||
[
|
[
|
||||||
'embeddingVector' => '[' . implode(',', $embeddingVector->vector->values) . ']',
|
'embeddingVector' => '[' . implode(',', $embeddingVector->vector->values) . ']',
|
||||||
'limit' => $limit,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
$embeddings = [];
|
$embeddings = [];
|
||||||
foreach ($result->fetchAllAssociative() as $row) {
|
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);
|
$embeddings[] = $this->mapRowToEmbedding($row);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new EmbeddingCollection($embeddings);
|
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(
|
$result = $this->connection->executeQuery(
|
||||||
'SELECT *
|
'SELECT *
|
||||||
|
|||||||
12
symfony.lock
12
symfony.lock
@ -136,6 +136,18 @@
|
|||||||
"ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
|
"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": {
|
"symfony/routing": {
|
||||||
"version": "7.2",
|
"version": "7.2",
|
||||||
"recipe": {
|
"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">
|
<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('') }}">
|
<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">
|
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -9,39 +10,380 @@
|
|||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const searchForm = document.getElementById('searchForm');
|
const searchForm = document.getElementById('searchForm');
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const searchButton = document.getElementById('searchButton');
|
||||||
|
const resultsContainer = document.getElementById('resultsContainer');
|
||||||
|
|
||||||
function encodeSearchQuery(query) {
|
function encodeSearchQuery(query) {
|
||||||
query = query.replace(/[^a-zA-Z0-9+\-\s]/g, '');
|
|
||||||
return encodeURIComponent(query);
|
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) {
|
searchForm.addEventListener('submit', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const query = searchInput.value.trim();
|
const query = searchInput.value.trim();
|
||||||
if (query) {
|
performSearch(query);
|
||||||
const encodedQuery = encodeSearchQuery(query);
|
|
||||||
window.location.href = `/s/${encodedQuery}`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle search button click
|
||||||
searchButton.addEventListener('click', function(e) {
|
searchButton.addEventListener('click', function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const query = searchInput.value.trim();
|
const query = searchInput.value.trim();
|
||||||
if (query) {
|
performSearch(query);
|
||||||
const encodedQuery = encodeSearchQuery(query);
|
|
||||||
window.location.href = `/s/${encodedQuery}`;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle Enter key press
|
||||||
searchInput.addEventListener('keypress', function(e) {
|
searchInput.addEventListener('keypress', function(e) {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const query = searchInput.value.trim();
|
const query = searchInput.value.trim();
|
||||||
if (query) {
|
performSearch(query);
|
||||||
const encodedQuery = encodeSearchQuery(query);
|
|
||||||
window.location.href = `/s/${encodedQuery}`;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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 {
|
.loading {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,4 +8,5 @@
|
|||||||
|
|
||||||
{% include '_components/search.html.twig' %}
|
{% include '_components/search.html.twig' %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="resultsContainer" class="results-container"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -6,8 +6,10 @@
|
|||||||
|
|
||||||
{% include '_components/search.html.twig' with { query: query } %}
|
{% include '_components/search.html.twig' with { query: query } %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tiles-grid">
|
{% if tiles is defined and tiles|length > 0 %}
|
||||||
{% include 'result/tiles/collection.html.twig' with { tiles: tiles } %}
|
<div id="resultsContainer" class="results-container">
|
||||||
</div>
|
{% include '_components/result.html.twig' with { tiles: tiles } %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user