HifiPlanet-Website/hifi/api/src/Controllers/SettingsController.php
Maaxxs a9aea99028 Fix data export/import: PHP's default upload limits rejected real exports
Root cause: the export embeds the whole gallery as base64 directly in the
JSON (that's how it stays a single self-contained file), which pushes a
realistic export well past 40MB. PHP's post_max_size/upload_max_filesize
default to values well under that on most hosting, so the browser's
upload got silently discarded before the app ever saw it - PHP clears
$_FILES and $_POST once post_max_size is exceeded, and the leftover raw
body in php://input is unparsed multipart data, not JSON, so it fell
into a generic "invalid file" 422 with no indication of what actually
went wrong. Reproduced locally with a real ~43MB export against the
previous 40M limit.

Raises the limits to 200M via two paths, since we don't know which PHP
SAPI the various hosting targets (All-Inkl, the Plesk test server) use:
hifi/.htaccess sets php_value overrides for classic Apache module PHP,
guarded by <IfModule> checks for several common module names so hosts
running PHP-FPM/CGI (which ignore php_value and would otherwise choke on
an unrecognized directive) skip the block instead of 500ing the entire
site; hifi/api/public/.user.ini covers exactly that FPM/CGI case, which
mod_php hosts in turn simply don't read.

Also makes SettingsController::importData() detect an oversized upload
by comparing Content-Length against the configured post_max_size, and
report the actual limit instead of the generic corrupt-file message -
so if some host's real limit is still too low, the admin sees why
instead of a dead end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 00:54:34 +02:00

484 lines
30 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Controllers;
use App\Config\Database;
use App\Support\Http;
use App\Support\Slug;
use PDO;
class SettingsController
{
// Nur diese Spalten pro Tabelle werden exportiert/importiert schützt vor
// manipulierten Spaltennamen in Import-Dateien und erlaubt trotzdem, dass
// Exporte aus älteren/neueren Versionen (mit weniger/mehr Spalten) importiert
// werden können (fehlende Spalten bekommen einfach ihren DB-Standardwert).
private const TABLE_COLUMNS = [
'brands' => ['id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
'car_models' => ['id', 'brand_id', 'name', 'slug', 'sort_order', 'created_at', 'updated_at'],
'packages' => ['id', 'car_model_id', 'name', 'slug', 'description', 'markup_type', 'markup_value', 'icon_name', 'tagline', 'price_text', 'is_featured', 'sort_order', 'created_at', 'updated_at'],
'package_products' => ['id', 'package_id', 'source_url', 'name_override', 'scraped_name', 'scraped_price', 'scraped_currency', 'scraped_image_url', 'price_updated_at', 'scrape_status', 'scrape_error', 'sort_order', 'created_at', 'updated_at'],
'package_upgrades' => ['id', 'package_id', 'name', 'description', 'price', 'sort_order', 'created_at', 'updated_at'],
'services' => ['id', 'icon_name', 'title', 'description', 'image_path', 'cta_label', 'cta_url', 'sort_order', 'created_at', 'updated_at'],
'faqs' => ['id', 'question_de', 'answer_de', 'question_en', 'answer_en', 'sort_order', 'created_at', 'updated_at'],
'gallery_brands' => ['id', 'name', 'slug', 'cover_image_path', 'sort_order', 'created_at', 'updated_at'],
'gallery_projects' => ['id', 'gallery_brand_id', 'name', 'slug', 'cover_image_path', 'sort_order', 'created_at', 'updated_at'],
'gallery_photos' => ['id', 'gallery_project_id', 'image_path', 'caption', 'sort_order', 'created_at', 'updated_at'],
];
// Kein const, da abhängig von der Umgebung (base_path unterscheidet sich
// zwischen lokalem /hifi und der Root-Domain auf IONOS).
private static function uploadsUrlPrefix(): string
{
$config = require __DIR__ . '/../../config/config.php';
return rtrim($config['app']['base_path'], '/') . '/api/uploads/';
}
public static function exportData(): void
{
$db = Database::connection();
$data = [];
foreach (array_keys(self::TABLE_COLUMNS) as $table) {
$data[$table] = $db->query("SELECT * FROM `$table`")->fetchAll();
}
$images = [];
$imageColumns = [
'services' => 'image_path',
'gallery_brands' => 'cover_image_path',
'gallery_projects' => 'cover_image_path',
'gallery_photos' => 'image_path',
];
foreach ($imageColumns as $table => $column) {
foreach ($data[$table] as $row) {
self::collectImage($row[$column] ?? null, $images);
}
}
$payload = [
'app' => 'hifiplanet',
'export_version' => 1,
'exported_at' => date('c'),
'data' => $data,
'images' => $images,
];
$filename = 'hifiplanet-export-' . date('Y-m-d_His') . '.json';
header('Content-Type: application/json; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $filename . '"');
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/** Wandelt eine php.ini-Groessenangabe ("200M", "1G", "512K") in Bytes um. */
private static function iniBytes(string $value): int
{
$value = trim($value);
if ($value === '') {
return 0;
}
$num = (float) $value;
return match (strtolower(substr($value, -1))) {
'g' => (int) ($num * 1024 * 1024 * 1024),
'm' => (int) ($num * 1024 * 1024),
'k' => (int) ($num * 1024),
default => (int) $value,
};
}
public static function importData(): void
{
if (!empty($_FILES['file'])) {
$uploadError = $_FILES['file']['error'];
if ($uploadError === UPLOAD_ERR_INI_SIZE || $uploadError === UPLOAD_ERR_FORM_SIZE) {
Http::error(
'Die Datei überschreitet das Upload-Limit dieses Servers (aktuell ' . ini_get('upload_max_filesize') .
'). Bitte den Hosting-Anbieter um ein höheres PHP-Upload-Limit bitten.',
422
);
}
if ($uploadError !== UPLOAD_ERR_OK) {
Http::error('Datei-Upload fehlgeschlagen (Fehlercode ' . $uploadError . ').', 422);
}
$raw = file_get_contents($_FILES['file']['tmp_name']);
} else {
// Ein Upload, der post_max_size ueberschreitet, wird von PHP nicht in $_FILES
// aufgenommen - der rohe Body ist ueber php://input aber trotzdem da (nur eben
// noch als unverarbeitetes Multipart-Gemisch, kein gueltiges JSON). Deshalb direkt
// Content-Length gegen das konfigurierte Limit pruefen, statt block auf einen
// (nicht garantiert leeren) Body zu vertrauen - sonst landet eine zu grosse, aber
// eigentlich intakte Export-Datei im irrefuehrenden generischen "ungueltige Datei"-Fehler.
$contentLength = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
$maxPost = self::iniBytes((string) ini_get('post_max_size'));
if ($maxPost > 0 && $contentLength > $maxPost) {
Http::error(
'Die Datei überschreitet das Größen-Limit dieses Servers (aktuell ' . ini_get('post_max_size') .
'). Bitte den Hosting-Anbieter um ein höheres PHP-Upload-Limit (post_max_size) bitten.',
422
);
}
$raw = file_get_contents('php://input');
}
$payload = json_decode((string) $raw, true);
if (!is_array($payload) || !isset($payload['data']) || !is_array($payload['data'])) {
Http::error('Ungültige oder beschädigte Import-Datei', 422);
}
$data = $payload['data'];
$images = is_array($payload['images'] ?? null) ? $payload['images'] : [];
$db = Database::connection();
$counts = [];
try {
$db->beginTransaction();
$db->exec('SET FOREIGN_KEY_CHECKS=0');
foreach (self::TABLE_COLUMNS as $table => $allowedColumns) {
$rows = is_array($data[$table] ?? null) ? $data[$table] : [];
$db->exec("DELETE FROM `$table`");
$counts[$table] = self::insertRows($db, $table, $allowedColumns, $rows);
}
self::restoreImages($images);
$db->exec('SET FOREIGN_KEY_CHECKS=1');
$db->commit();
} catch (\Throwable $e) {
$db->rollBack();
$db->exec('SET FOREIGN_KEY_CHECKS=1');
Http::error('Import fehlgeschlagen, es wurde nichts verändert: ' . $e->getMessage(), 500);
}
Http::send(['ok' => true, 'counts' => $counts, 'images_restored' => count($images)]);
}
public static function resetServicesToDefaults(): void
{
$db = Database::connection();
$defaults = self::defaultServices();
$db->exec('DELETE FROM services');
$stmt = $db->prepare(
'INSERT INTO services (icon_name, title, description, image_path, cta_label, cta_url, sort_order) VALUES (?,?,?,?,?,?,?)'
);
foreach ($defaults as $row) {
$stmt->execute($row);
}
Http::send(['ok' => true, 'count' => count($defaults)]);
}
public static function resetFaqsToDefaults(): void
{
$db = Database::connection();
$defaults = self::defaultFaqs();
$db->exec('DELETE FROM faqs');
$stmt = $db->prepare(
'INSERT INTO faqs (question_de, answer_de, question_en, answer_en, sort_order) VALUES (?,?,?,?,?)'
);
foreach ($defaults as $row) {
$stmt->execute($row);
}
Http::send(['ok' => true, 'count' => count($defaults)]);
}
/** Löscht Galerie-Marken (Foreign-Key-Kaskade räumt Projekte & Fotos automatisch mit auf). */
public static function resetGalleryToDefaults(): void
{
$db = Database::connection();
$db->exec('DELETE FROM gallery_brands');
$photoCount = self::seedGalleryDefaults($db);
Http::send(['ok' => true, 'photos' => $photoCount]);
}
/** Löscht Marken (Foreign-Key-Kaskade räumt Modelle, Pakete, Produkte & Upgrades automatisch mit auf). */
public static function resetCatalog(): void
{
$db = Database::connection();
$brandCount = (int) $db->query('SELECT COUNT(*) FROM brands')->fetchColumn();
$db->exec('DELETE FROM brands');
Http::send(['ok' => true, 'brands_removed' => $brandCount]);
}
public static function resetAll(): void
{
$db = Database::connection();
$brandCount = (int) $db->query('SELECT COUNT(*) FROM brands')->fetchColumn();
$db->exec('DELETE FROM brands');
$defaults = self::defaultServices();
$db->exec('DELETE FROM services');
$stmt = $db->prepare(
'INSERT INTO services (icon_name, title, description, image_path, cta_label, cta_url, sort_order) VALUES (?,?,?,?,?,?,?)'
);
foreach ($defaults as $row) {
$stmt->execute($row);
}
$faqDefaults = self::defaultFaqs();
$db->exec('DELETE FROM faqs');
$faqStmt = $db->prepare(
'INSERT INTO faqs (question_de, answer_de, question_en, answer_en, sort_order) VALUES (?,?,?,?,?)'
);
foreach ($faqDefaults as $row) {
$faqStmt->execute($row);
}
$db->exec('DELETE FROM gallery_brands');
$galleryPhotoCount = self::seedGalleryDefaults($db);
Http::send([
'ok' => true,
'brands_removed' => $brandCount,
'services_reset' => count($defaults),
'faqs_reset' => count($faqDefaults),
'gallery_photos_reset' => $galleryPhotoCount,
]);
}
/** @param string[] $allowedColumns @param array<int,array<string,mixed>> $rows */
private static function insertRows(PDO $db, string $table, array $allowedColumns, array $rows): int
{
if (!$rows) {
return 0;
}
$columns = array_values(array_intersect(array_keys($rows[0]), $allowedColumns));
if (!$columns) {
return 0;
}
$columnList = '`' . implode('`,`', $columns) . '`';
$placeholders = '(' . implode(',', array_fill(0, count($columns), '?')) . ')';
$stmt = $db->prepare("INSERT INTO `$table` ($columnList) VALUES $placeholders");
foreach ($rows as $row) {
$values = array_map(fn($col) => $row[$col] ?? null, $columns);
$stmt->execute($values);
}
return count($rows);
}
private static function collectImage(?string $path, array &$images): void
{
if (!$path || isset($images[$path]) || !str_starts_with($path, self::uploadsUrlPrefix())) {
return;
}
$filename = basename($path);
$fullPath = __DIR__ . '/../../uploads/' . $filename;
if (is_file($fullPath)) {
$images[$path] = base64_encode((string) file_get_contents($fullPath));
}
}
/** @param array<string,string> $images */
private static function restoreImages(array $images): void
{
$uploadsDir = __DIR__ . '/../../uploads';
foreach ($images as $path => $base64) {
$filename = basename((string) $path);
if ($filename === '' || str_contains($filename, '..')) {
continue;
}
file_put_contents($uploadsDir . '/' . $filename, base64_decode((string) $base64));
}
}
private static function defaultServices(): array
{
return [
['car', 'Car-Hifi', 'Individuelle Sound-Umbauten vom dezenten Upgrade bis zum kompromisslosen High-End-System konfigurierbar direkt auf unserer Seite.', self::uploadsUrlPrefix() . 'seed-leistung-car-hifi.jpg', 'Fahrzeug konfigurieren', '/fahrzeuge', 0],
['caravan', 'Wohnmobil & Caravan', 'Sound- und Elektronik-Lösungen speziell für Reisemobile und Caravans reversibel oder fest verbaut.', self::uploadsUrlPrefix() . 'seed-leistung-wohnmobil.jpg', null, null, 1],
['car-front', 'Oldtimer', 'Zeitgemäßer Klang für Klassiker wir modernisieren die Anlage, ohne den Charakter deines Oldtimers zu verlieren.', self::uploadsUrlPrefix() . 'seed-leistung-oldtimer.jpg', null, null, 2],
['cog', 'CNC Zerspanen', 'Fräsen, Bohren, Schneiden und Schleifen auf einer Fläche von 1500 × 3000 mm für Prototypen, Kleinserien und Großauflagen. Massivholz, Verbundwerkstoffe, Plattenmaterial und mehr.', self::uploadsUrlPrefix() . 'seed-leistung-cnc-zerspanen.jpg', null, null, 3],
['zap', 'CNC Lasertechnik', 'Präzises Laserschneiden und -gravieren für Leder, Glas, Acryl, Holz, Karton und mehr für Einzelstücke ebenso wie Serienfertigung.', self::uploadsUrlPrefix() . 'seed-leistung-cnc-laser.jpg', null, null, 4],
['boxes', '3D-Druck', 'Über 20 Drucker (FDM & SLA) für individuelle Halterungen, Blenden und Kleinserien Fertigung meist in 13 Tagen, auf Wunsch auch farbig.', self::uploadsUrlPrefix() . 'seed-leistung-3d-druck.jpg', null, null, 5],
['shield-alert', 'Alarmanlagen', 'Zuverlässiger Diebstahlschutz für dein Fahrzeug, abgestimmt auf deine Ansprüche und dein Budget.', self::uploadsUrlPrefix() . 'seed-leistung-alarmanlagen.jpg', null, null, 6],
['video', 'Dash Cams', 'Beweissichere Aufzeichnung fürs Auto wir beraten dich zur passenden Kamera und übernehmen den unauffälligen Einbau.', self::uploadsUrlPrefix() . 'seed-leistung-dashcam.jpg', null, null, 7],
];
}
private static function defaultFaqs(): array
{
return [
[
'Was kostet eine Beratung?',
'Beratung und Preisanfrage sind für dich komplett kostenlos und unverbindlich.',
'How much does a consultation cost?',
'A consultation and price estimate are completely free and non-binding.',
0,
],
[
'Muss ich mein Fahrzeug vorbeibringen?',
'Für eine erste Einschätzung reicht oft deine Anfrage über die Website. Für den Einbau selbst vereinbaren wir gemeinsam einen Termin bei uns in Amorbach.',
'Do I need to bring my vehicle in?',
"For an initial assessment, your inquiry through the website is often enough. For the installation itself, we'll schedule an appointment together at our workshop in Amorbach.",
1,
],
[
'Wie lange dauert ein Umbau?',
'Das hängt vom Umfang ab von einem einfachen Lautsprecher-Tausch in wenigen Stunden bis zum aufwendigen Komplettumbau über mehrere Tage.',
'How long does an installation take?',
'It depends on the scope from a simple speaker swap in a few hours to an elaborate full installation over several days.',
2,
],
[
'Bietet ihr auch Lösungen für Leasingfahrzeuge?',
'Ja, auf Wunsch bauen wir reversibel um, sodass dein Fahrzeug bei Rückgabe wieder in den Originalzustand versetzt werden kann.',
'Do you offer solutions for leased vehicles?',
'Yes, on request we install everything reversibly, so your vehicle can be returned to its original condition when handed back.',
3,
],
[
'Arbeitet ihr nur mit bestimmten Marken?',
'Nein, wir sind herstellerunabhängig und wählen die Komponenten, die am besten zu deinem Anspruch und Budget passen.',
'Do you only work with certain brands?',
"No, we're brand-independent and choose the components that best fit your needs and budget.",
4,
],
[
'Was ist, wenn mein Fahrzeug nicht gelistet ist?',
'Kein Problem schreib uns einfach über das Kontaktformular, wir finden für jedes Fahrzeug eine passende Lösung.',
"What if my vehicle isn't listed?",
"No problem just message us through the contact form, we'll find a suitable solution for any vehicle.",
5,
],
];
}
/** Legt Galerie-Marken/-Projekte/-Fotos aus {@see defaultGalleryData()} neu an (Tabellen müssen leer sein). Gibt die Foto-Anzahl zurück. */
private static function seedGalleryDefaults(PDO $db): int
{
$photoCount = 0;
$brandStmt = $db->prepare('INSERT INTO gallery_brands (name, slug, cover_image_path, sort_order) VALUES (?,?,?,?)');
$projectStmt = $db->prepare('INSERT INTO gallery_projects (gallery_brand_id, name, slug, cover_image_path, sort_order) VALUES (?,?,?,?,?)');
$photoStmt = $db->prepare('INSERT INTO gallery_photos (gallery_project_id, image_path, sort_order) VALUES (?,?,?)');
$updateBrandCoverStmt = $db->prepare('UPDATE gallery_brands SET cover_image_path = ? WHERE id = ?');
$brandIndex = 0;
foreach (self::defaultGalleryData() as $brand) {
$brandIndex++;
$brandStmt->execute([$brand['name'], Slug::make($brand['name']), null, $brandIndex]);
$brandId = (int) $db->lastInsertId();
$brandCover = null;
$projectIndex = 0;
foreach ($brand['projects'] as $project) {
$projectIndex++;
$photoUrls = array_map(fn($filename) => self::uploadsUrlPrefix() . $filename, $project['photos']);
$projectCover = $photoUrls[0] ?? null;
$brandCover ??= $projectCover;
$projectStmt->execute([$brandId, $project['name'], Slug::make($project['name']), $projectCover, $projectIndex]);
$projectId = (int) $db->lastInsertId();
$photoIndex = 0;
foreach ($photoUrls as $url) {
$photoIndex++;
$photoStmt->execute([$projectId, $url, $photoIndex]);
$photoCount++;
}
}
if ($brandCover !== null) {
$updateBrandCoverStmt->execute([$brandCover, $brandId]);
}
}
return $photoCount;
}
/** Bildergalerie der alten hifi-planet.de-Seite (WordPress), einmalig übernommen. */
private static function defaultGalleryData(): array
{
return [
[
'name' => 'Audi', 'projects' => [
['name' => 'Audi TT', 'photos' => ['seed-galerie-audi-audi-tt-1.jpg', 'seed-galerie-audi-audi-tt-2.jpg', 'seed-galerie-audi-audi-tt-3.jpg', 'seed-galerie-audi-audi-tt-4.jpg', 'seed-galerie-audi-audi-tt-5.jpg', 'seed-galerie-audi-audi-tt-6.jpg', 'seed-galerie-audi-audi-tt-7.jpg', 'seed-galerie-audi-audi-tt-8.jpg', 'seed-galerie-audi-audi-tt-9.jpg']],
['name' => 'Audi RS Q3', 'photos' => ['seed-galerie-audi-audio-rs-q3-1.jpg', 'seed-galerie-audi-audio-rs-q3-2.jpg', 'seed-galerie-audi-audio-rs-q3-3.jpg', 'seed-galerie-audi-audio-rs-q3-4.jpg', 'seed-galerie-audi-audio-rs-q3-5.jpg', 'seed-galerie-audi-audio-rs-q3-6.jpg', 'seed-galerie-audi-audio-rs-q3-7.jpg', 'seed-galerie-audi-audio-rs-q3-8.jpg', 'seed-galerie-audi-audio-rs-q3-9.jpg', 'seed-galerie-audi-audio-rs-q3-10.jpg']],
],
],
[
'name' => 'BMW', 'projects' => [
['name' => 'F10 Touring', 'photos' => ['seed-galerie-bmw-f10-touring-1.jpg', 'seed-galerie-bmw-f10-touring-2.jpg', 'seed-galerie-bmw-f10-touring-3.jpg', 'seed-galerie-bmw-f10-touring-4.jpg', 'seed-galerie-bmw-f10-touring-5.jpg', 'seed-galerie-bmw-f10-touring-6.jpg', 'seed-galerie-bmw-f10-touring-7.jpg', 'seed-galerie-bmw-f10-touring-8.jpg']],
['name' => 'Mini Cooper RS', 'photos' => ['seed-galerie-bmw-mini-cooper-rs-1.jpg', 'seed-galerie-bmw-mini-cooper-rs-2.jpg', 'seed-galerie-bmw-mini-cooper-rs-3.jpg']],
],
],
[
'name' => 'Elektro-Autos', 'projects' => [
['name' => 'Hyundai Ioniq 5', 'photos' => ['seed-galerie-elektro-autos-hyundai-ioniq-5-1.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-2.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-3.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-4.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-5.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-6.jpg', 'seed-galerie-elektro-autos-hyundai-ioniq-5-7.jpg']],
['name' => 'Tesla Model Y', 'photos' => ['seed-galerie-elektro-autos-tesla-model-y-1.jpg', 'seed-galerie-elektro-autos-tesla-model-y-2.jpg', 'seed-galerie-elektro-autos-tesla-model-y-3.jpg', 'seed-galerie-elektro-autos-tesla-model-y-4.jpg', 'seed-galerie-elektro-autos-tesla-model-y-5.jpg', 'seed-galerie-elektro-autos-tesla-model-y-6.jpg']],
],
],
[
'name' => 'Fiat / Ford', 'projects' => [
['name' => 'Fiesta MK7', 'photos' => ['seed-galerie-fiat-ford-fiesta-mk7-1.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-2.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-3.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-4.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-5.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-6.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-7.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-8.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-9.jpg', 'seed-galerie-fiat-ford-fiesta-mk7-10.jpg']],
['name' => 'Mustang VI', 'photos' => ['seed-galerie-fiat-ford-mustang-vi-1.jpg', 'seed-galerie-fiat-ford-mustang-vi-2.jpg', 'seed-galerie-fiat-ford-mustang-vi-3.jpg', 'seed-galerie-fiat-ford-mustang-vi-4.jpg', 'seed-galerie-fiat-ford-mustang-vi-5.jpg', 'seed-galerie-fiat-ford-mustang-vi-6.jpg', 'seed-galerie-fiat-ford-mustang-vi-7.jpg']],
['name' => 'Mustang V', 'photos' => ['seed-galerie-fiat-ford-mustang-1.jpg', 'seed-galerie-fiat-ford-mustang-2.jpg', 'seed-galerie-fiat-ford-mustang-3.jpg', 'seed-galerie-fiat-ford-mustang-4.jpg', 'seed-galerie-fiat-ford-mustang-5.jpg', 'seed-galerie-fiat-ford-mustang-6.jpg']],
],
],
[
'name' => 'Hyundai', 'projects' => [
['name' => 'i30', 'photos' => ['seed-galerie-hyundai-i30-1.jpg', 'seed-galerie-hyundai-i30-2.jpg', 'seed-galerie-hyundai-i30-3.jpg', 'seed-galerie-hyundai-i30-4.jpg', 'seed-galerie-hyundai-i30-5.jpg']],
],
],
[
'name' => 'Mercedes-Benz', 'projects' => [
['name' => 'GLS', 'photos' => ['seed-galerie-mercedes-benz-gls-1.jpg', 'seed-galerie-mercedes-benz-gls-2.jpg', 'seed-galerie-mercedes-benz-gls-3.jpg', 'seed-galerie-mercedes-benz-gls-4.jpg', 'seed-galerie-mercedes-benz-gls-5.jpg']],
['name' => 'SLK R 170', 'photos' => ['seed-galerie-mercedes-benz-slk-r-170-1.jpg', 'seed-galerie-mercedes-benz-slk-r-170-2.jpg', 'seed-galerie-mercedes-benz-slk-r-170-3.jpg', 'seed-galerie-mercedes-benz-slk-r-170-4.jpg', 'seed-galerie-mercedes-benz-slk-r-170-5.jpg', 'seed-galerie-mercedes-benz-slk-r-170-6.jpg', 'seed-galerie-mercedes-benz-slk-r-170-7.jpg', 'seed-galerie-mercedes-benz-slk-r-170-8.jpg']],
['name' => 'Vito', 'photos' => ['seed-galerie-mercedes-benz-vito-1.jpg', 'seed-galerie-mercedes-benz-vito-2.jpg', 'seed-galerie-mercedes-benz-vito-3.jpg', 'seed-galerie-mercedes-benz-vito-4.jpg', 'seed-galerie-mercedes-benz-vito-5.jpg', 'seed-galerie-mercedes-benz-vito-6.jpg', 'seed-galerie-mercedes-benz-vito-7.jpg', 'seed-galerie-mercedes-benz-vito-8.jpg']],
],
],
[
'name' => 'Opel', 'projects' => [
['name' => 'Astra J', 'photos' => ['seed-galerie-opel-astra-j-1.jpg', 'seed-galerie-opel-astra-j-2.jpg', 'seed-galerie-opel-astra-j-3.jpg', 'seed-galerie-opel-astra-j-4.jpg', 'seed-galerie-opel-astra-j-5.jpg', 'seed-galerie-opel-astra-j-6.jpg']],
['name' => 'Meriva A', 'photos' => ['seed-galerie-opel-meriva-a-1.jpg', 'seed-galerie-opel-meriva-a-2.jpg', 'seed-galerie-opel-meriva-a-3.jpg', 'seed-galerie-opel-meriva-a-4.jpg', 'seed-galerie-opel-meriva-a-5.jpg', 'seed-galerie-opel-meriva-a-6.jpg', 'seed-galerie-opel-meriva-a-7.jpg', 'seed-galerie-opel-meriva-a-8.jpg', 'seed-galerie-opel-meriva-a-9.jpg']],
['name' => 'Insignia A', 'photos' => ['seed-galerie-opel-opel-insignia-a-1.jpg', 'seed-galerie-opel-opel-insignia-a-2.jpg', 'seed-galerie-opel-opel-insignia-a-3.jpg', 'seed-galerie-opel-opel-insignia-a-4.jpg', 'seed-galerie-opel-opel-insignia-a-5.jpg', 'seed-galerie-opel-opel-insignia-a-6.jpg', 'seed-galerie-opel-opel-insignia-a-7.jpg']],
],
],
[
'name' => 'Peugeot', 'projects' => [
['name' => '208 HDI', 'photos' => ['seed-galerie-peugeot-208-hdi-1.jpg', 'seed-galerie-peugeot-208-hdi-2.jpg', 'seed-galerie-peugeot-208-hdi-3.jpg', 'seed-galerie-peugeot-208-hdi-4.jpg', 'seed-galerie-peugeot-208-hdi-5.jpg', 'seed-galerie-peugeot-208-hdi-6.jpg', 'seed-galerie-peugeot-208-hdi-7.jpg', 'seed-galerie-peugeot-208-hdi-8.jpg', 'seed-galerie-peugeot-208-hdi-9.jpg', 'seed-galerie-peugeot-208-hdi-10.jpg', 'seed-galerie-peugeot-208-hdi-11.jpg', 'seed-galerie-peugeot-208-hdi-12.jpg']],
],
],
[
'name' => 'Seat / Skoda', 'projects' => [
['name' => 'Alhambra', 'photos' => ['seed-galerie-seat-skoda-alhambra-1.jpg', 'seed-galerie-seat-skoda-alhambra-2.jpg', 'seed-galerie-seat-skoda-alhambra-3.jpg', 'seed-galerie-seat-skoda-alhambra-4.jpg', 'seed-galerie-seat-skoda-alhambra-5.jpg', 'seed-galerie-seat-skoda-alhambra-6.jpg', 'seed-galerie-seat-skoda-alhambra-7.jpg', 'seed-galerie-seat-skoda-alhambra-8.jpg']],
['name' => 'Octavia', 'photos' => ['seed-galerie-seat-skoda-octavia-1.jpg', 'seed-galerie-seat-skoda-octavia-2.jpg', 'seed-galerie-seat-skoda-octavia-3.jpg', 'seed-galerie-seat-skoda-octavia-4.jpg', 'seed-galerie-seat-skoda-octavia-5.jpg', 'seed-galerie-seat-skoda-octavia-6.jpg']],
],
],
[
'name' => 'Smart', 'projects' => [
['name' => 'Fortwo 453', 'photos' => ['seed-galerie-smart-for-two-453-1.jpg', 'seed-galerie-smart-for-two-453-2.jpg', 'seed-galerie-smart-for-two-453-3.jpg', 'seed-galerie-smart-for-two-453-4.jpg', 'seed-galerie-smart-for-two-453-5.jpg', 'seed-galerie-smart-for-two-453-6.jpg', 'seed-galerie-smart-for-two-453-7.jpg', 'seed-galerie-smart-for-two-453-8.jpg']],
],
],
[
'name' => 'Suzuki', 'projects' => [
['name' => 'Swift', 'photos' => ['seed-galerie-suzuki-swift-1.jpg', 'seed-galerie-suzuki-swift-2.jpg', 'seed-galerie-suzuki-swift-3.jpg', 'seed-galerie-suzuki-swift-4.jpg', 'seed-galerie-suzuki-swift-5.jpg']],
],
],
[
'name' => 'Toyota', 'projects' => [
['name' => 'Corolla', 'photos' => ['seed-galerie-toyota-crolla-1.jpg', 'seed-galerie-toyota-crolla-2.jpg', 'seed-galerie-toyota-crolla-3.jpg', 'seed-galerie-toyota-crolla-4.jpg', 'seed-galerie-toyota-crolla-5.jpg', 'seed-galerie-toyota-crolla-6.jpg']],
],
],
[
'name' => 'Volkswagen', 'projects' => [
['name' => 'Golf 1 Cabrio', 'photos' => ['seed-galerie-volkswagen-golf-1-cabrio-1.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-2.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-3.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-4.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-5.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-6.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-7.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-8.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-9.jpg', 'seed-galerie-volkswagen-golf-1-cabrio-10.jpg']],
['name' => 'Golf 4', 'photos' => ['seed-galerie-volkswagen-golf-4-1.jpg', 'seed-galerie-volkswagen-golf-4-2.jpg', 'seed-galerie-volkswagen-golf-4-3.jpg', 'seed-galerie-volkswagen-golf-4-4.jpg', 'seed-galerie-volkswagen-golf-4-5.jpg', 'seed-galerie-volkswagen-golf-4-6.jpg']],
['name' => 'Golf 6', 'photos' => ['seed-galerie-volkswagen-golf-6-1.jpg', 'seed-galerie-volkswagen-golf-6-2.jpg', 'seed-galerie-volkswagen-golf-6-3.jpg', 'seed-galerie-volkswagen-golf-6-4.jpg', 'seed-galerie-volkswagen-golf-6-5.jpg', 'seed-galerie-volkswagen-golf-6-6.jpg', 'seed-galerie-volkswagen-golf-6-7.jpg', 'seed-galerie-volkswagen-golf-6-8.jpg', 'seed-galerie-volkswagen-golf-6-9.jpg', 'seed-galerie-volkswagen-golf-6-10.jpg', 'seed-galerie-volkswagen-golf-6-11.jpg', 'seed-galerie-volkswagen-golf-6-12.jpg']],
['name' => 'T5 Multivan', 'photos' => ['seed-galerie-volkswagen-t5-multivan-1.jpg', 'seed-galerie-volkswagen-t5-multivan-2.jpg', 'seed-galerie-volkswagen-t5-multivan-3.jpg', 'seed-galerie-volkswagen-t5-multivan-4.jpg', 'seed-galerie-volkswagen-t5-multivan-5.jpg', 'seed-galerie-volkswagen-t5-multivan-6.jpg', 'seed-galerie-volkswagen-t5-multivan-7.jpg', 'seed-galerie-volkswagen-t5-multivan-8.jpg']],
['name' => 'VW Crafter', 'photos' => ['seed-galerie-volkswagen-vw-crafter-1.jpg', 'seed-galerie-volkswagen-vw-crafter-2.jpg', 'seed-galerie-volkswagen-vw-crafter-3.jpg']],
['name' => 'VW T6.1', 'photos' => ['seed-galerie-volkswagen-vw-t6-1-1.jpg', 'seed-galerie-volkswagen-vw-t6-1-2.jpg', 'seed-galerie-volkswagen-vw-t6-1-3.jpg', 'seed-galerie-volkswagen-vw-t6-1-4.jpg']],
['name' => 'VW T6', 'photos' => ['seed-galerie-volkswagen-vw-t6-1.jpg', 'seed-galerie-volkswagen-vw-t6-2.jpg', 'seed-galerie-volkswagen-vw-t6-3.jpg', 'seed-galerie-volkswagen-vw-t6-4.jpg', 'seed-galerie-volkswagen-vw-t6-5.jpg']],
],
],
];
}
}