Fix: Updating MIgrateImportCommand to accept language for import
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
namespace Modules\Core\Command;
|
namespace Modules\Core\Command;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
use Lunar\Models\Language;
|
||||||
use Modules\Core\MigrateImport\ImportSpec;
|
use Modules\Core\MigrateImport\ImportSpec;
|
||||||
use Modules\Core\MigrateImport\RunMigrateImportJob;
|
use Modules\Core\MigrateImport\RunMigrateImportJob;
|
||||||
|
|
||||||
@@ -62,11 +63,29 @@ class MigrateImportCommand extends Command
|
|||||||
$credentials = null;
|
$credentials = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Shopify's own product export is a flat CSV — one Title/Body
|
||||||
|
// (HTML)/etc. column per row, no per-locale columns at all — so
|
||||||
|
// its text is necessarily written in exactly one language, and
|
||||||
|
// there is no reliable way to detect which one from the file
|
||||||
|
// itself. Modules\Core\MigrateImport\ImportLocale::code() used to
|
||||||
|
// (as its former name, DefaultLocale, admits) assume it always
|
||||||
|
// matched this store's own Lunar\Models\
|
||||||
|
// Language::getDefault(), which is often wrong (a store's default
|
||||||
|
// admin/storefront language and the language a given export
|
||||||
|
// happens to be written in are two independent facts) — every
|
||||||
|
// imported product's name/description then saved silently under
|
||||||
|
// the wrong language, invisible unless that language happened to
|
||||||
|
// also be selected when viewing/editing the product afterward.
|
||||||
|
$locale = $source === 'shopify' && $type === 'export'
|
||||||
|
? $this->askImportLocale()
|
||||||
|
: null;
|
||||||
|
|
||||||
$spec = new ImportSpec(
|
$spec = new ImportSpec(
|
||||||
source: $source,
|
source: $source,
|
||||||
type: $type,
|
type: $type,
|
||||||
filePath: $filePath,
|
filePath: $filePath,
|
||||||
credentials: $credentials,
|
credentials: $credentials,
|
||||||
|
locale: $locale,
|
||||||
);
|
);
|
||||||
|
|
||||||
RunMigrateImportJob::dispatch($spec);
|
RunMigrateImportJob::dispatch($spec);
|
||||||
@@ -74,6 +93,26 @@ class MigrateImportCommand extends Command
|
|||||||
$this->info('Import queued.');
|
$this->info('Import queued.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choices come from Language::all() — the same list an admin manages
|
||||||
|
* from the Filament panel (Settings > Languages) — not a hardcoded
|
||||||
|
* set, so a language this store doesn't have yet simply isn't
|
||||||
|
* offered here; the hint below says where to add it instead of this
|
||||||
|
* command silently accepting an arbitrary code Lunar has no row for.
|
||||||
|
*/
|
||||||
|
private function askImportLocale(): string
|
||||||
|
{
|
||||||
|
$languages = Language::orderBy('default', 'desc')->get(['code', 'name']);
|
||||||
|
|
||||||
|
return $this->choice(
|
||||||
|
"Which language is the export file's own text (product titles, descriptions, etc.) written in?\n".
|
||||||
|
' (Not necessarily this store\'s default language — the two are independent. '.
|
||||||
|
"If the language you need isn't listed, add it first from the admin panel under Languages.)",
|
||||||
|
$languages->mapWithKeys(fn (Language $language) => [$language->code => "{$language->name} ({$language->code})"])->all(),
|
||||||
|
$languages->first()?->code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Answers are relative to storage/app/private/imports (e.g. "shopify" or
|
// Answers are relative to storage/app/private/imports (e.g. "shopify" or
|
||||||
// "shopify/products_export.csv"); absolute paths are used as-is. A
|
// "shopify/products_export.csv"); absolute paths are used as-is. A
|
||||||
// directory answer picks the first CSV file found inside it.
|
// directory answer picks the first CSV file found inside it.
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Modules\Core\MigrateImport;
|
|
||||||
|
|
||||||
use Lunar\Models\Language;
|
|
||||||
|
|
||||||
class DefaultLocale
|
|
||||||
{
|
|
||||||
public static function code(): string
|
|
||||||
{
|
|
||||||
return Language::getDefault()->code;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\MigrateImport;
|
||||||
|
|
||||||
|
use Lunar\Models\Language;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The language every resolver in an import run writes product-facing text
|
||||||
|
* under (name, description, option names, ...) — NOT necessarily this
|
||||||
|
* store's own Lunar\Models\Language::getDefault(). An export file has no
|
||||||
|
* reliable way to declare its own language, and a store's default admin/
|
||||||
|
* storefront language is an independent fact from whatever language a
|
||||||
|
* given export happens to be written in — conflating the two (this
|
||||||
|
* class's own former name, DefaultLocale, said as much) used to silently
|
||||||
|
* save every imported product's text under the wrong language, invisible
|
||||||
|
* unless that language was also the one selected while viewing/editing
|
||||||
|
* the product afterward.
|
||||||
|
*
|
||||||
|
* Set once per import run from the operator's own answer (see
|
||||||
|
* Command\MigrateImportCommand::askImportLocale(), threaded through
|
||||||
|
* ImportSpec::$locale) at the top of Importer::import() — every resolver
|
||||||
|
* downstream (ProductAttributeResolver, ProductOptionResolver,
|
||||||
|
* CollectionResolver, ImportAttributeResolver) calls code() exactly as
|
||||||
|
* before, unaware anything changed. Falls back to Language::getDefault()
|
||||||
|
* only when nothing was ever set (e.g. an import path with no locale
|
||||||
|
* concept of its own — JudgeMe's reviews-only export never calls set()).
|
||||||
|
*/
|
||||||
|
class ImportLocale
|
||||||
|
{
|
||||||
|
private static ?string $code = null;
|
||||||
|
|
||||||
|
public static function set(string $code): void
|
||||||
|
{
|
||||||
|
self::$code = $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function code(): string
|
||||||
|
{
|
||||||
|
return self::$code ?? Language::getDefault()->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A static property outlives a single request only inside a
|
||||||
|
* long-running worker process, where a queued job for one import
|
||||||
|
* must not leak its locale into the next — called at the end of
|
||||||
|
* every Importer::import() run, success or failure.
|
||||||
|
*/
|
||||||
|
public static function reset(): void
|
||||||
|
{
|
||||||
|
self::$code = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,22 @@ namespace Modules\Core\MigrateImport;
|
|||||||
|
|
||||||
class ImportSpec
|
class ImportSpec
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @param ?string $locale The language the export file's own text
|
||||||
|
* (product titles, descriptions, option names, ...) is actually
|
||||||
|
* written in — asked of the operator at import time (see
|
||||||
|
* Command\MigrateImportCommand), since an export has no reliable
|
||||||
|
* way to declare its own language and it does not necessarily match
|
||||||
|
* this store's Lunar\Models\Language::getDefault(). Null for a
|
||||||
|
* source/type this doesn't apply to (e.g. an API-based import with
|
||||||
|
* no free-text file to attribute a single language to).
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public readonly string $source,
|
public readonly string $source,
|
||||||
public readonly string $type,
|
public readonly string $type,
|
||||||
public readonly ?string $filePath = null,
|
public readonly ?string $filePath = null,
|
||||||
public readonly ?array $credentials = null,
|
public readonly ?array $credentials = null,
|
||||||
|
public readonly ?string $locale = null,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use Lunar\FieldTypes\TranslatedText;
|
|||||||
use Lunar\Models\Collection;
|
use Lunar\Models\Collection;
|
||||||
use Lunar\Models\CollectionGroup;
|
use Lunar\Models\CollectionGroup;
|
||||||
use Lunar\Models\Product;
|
use Lunar\Models\Product;
|
||||||
use Modules\Core\MigrateImport\DefaultLocale;
|
use Modules\Core\MigrateImport\ImportLocale;
|
||||||
|
|
||||||
class CollectionResolver
|
class CollectionResolver
|
||||||
{
|
{
|
||||||
@@ -45,7 +45,7 @@ class CollectionResolver
|
|||||||
'collection_group_id' => $group->id,
|
'collection_group_id' => $group->id,
|
||||||
'attribute_data' => [
|
'attribute_data' => [
|
||||||
'name' => new TranslatedText(collect([
|
'name' => new TranslatedText(collect([
|
||||||
DefaultLocale::code() => new Text($name),
|
ImportLocale::code() => new Text($name),
|
||||||
])),
|
])),
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use Lunar\Models\Attribute;
|
|||||||
use Lunar\Models\AttributeGroup;
|
use Lunar\Models\AttributeGroup;
|
||||||
use Lunar\Models\Product;
|
use Lunar\Models\Product;
|
||||||
use Lunar\Models\ProductType;
|
use Lunar\Models\ProductType;
|
||||||
use Modules\Core\MigrateImport\DefaultLocale;
|
use Modules\Core\MigrateImport\ImportLocale;
|
||||||
|
|
||||||
class ImportAttributeResolver
|
class ImportAttributeResolver
|
||||||
{
|
{
|
||||||
@@ -40,7 +40,7 @@ class ImportAttributeResolver
|
|||||||
[
|
[
|
||||||
'attribute_group_id' => $group->id,
|
'attribute_group_id' => $group->id,
|
||||||
'position' => $nextPosition++,
|
'position' => $nextPosition++,
|
||||||
'name' => [DefaultLocale::code() => $definition['label']],
|
'name' => [ImportLocale::code() => $definition['label']],
|
||||||
'section' => 'main',
|
'section' => 'main',
|
||||||
'type' => $definition['type'],
|
'type' => $definition['type'],
|
||||||
'required' => false,
|
'required' => false,
|
||||||
@@ -49,7 +49,7 @@ class ImportAttributeResolver
|
|||||||
? ['richtext' => false]
|
? ['richtext' => false]
|
||||||
: [],
|
: [],
|
||||||
'system' => false,
|
'system' => false,
|
||||||
'description' => [DefaultLocale::code() => ''],
|
'description' => [ImportLocale::code() => ''],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class ImportAttributeResolver
|
|||||||
return AttributeGroup::firstOrCreate(
|
return AttributeGroup::firstOrCreate(
|
||||||
['attributable_type' => Product::morphName(), 'handle' => 'import'],
|
['attributable_type' => Product::morphName(), 'handle' => 'import'],
|
||||||
[
|
[
|
||||||
'name' => [DefaultLocale::code() => 'Additional Details'],
|
'name' => [ImportLocale::code() => 'Additional Details'],
|
||||||
'position' => 100,
|
'position' => 100,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use Lunar\FieldTypes\Number;
|
|||||||
use Lunar\FieldTypes\Text;
|
use Lunar\FieldTypes\Text;
|
||||||
use Lunar\FieldTypes\TranslatedText;
|
use Lunar\FieldTypes\TranslatedText;
|
||||||
use Lunar\Models\ProductType;
|
use Lunar\Models\ProductType;
|
||||||
use Modules\Core\MigrateImport\DefaultLocale;
|
use Modules\Core\MigrateImport\ImportLocale;
|
||||||
|
|
||||||
class ProductAttributeResolver
|
class ProductAttributeResolver
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ class ProductAttributeResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
return new TranslatedText(collect([
|
return new TranslatedText(collect([
|
||||||
DefaultLocale::code() => new Text($value),
|
ImportLocale::code() => new Text($value),
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Modules\Core\MigrateImport\Shopify\Resolvers;
|
|||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Lunar\Models\ProductOption;
|
use Lunar\Models\ProductOption;
|
||||||
use Lunar\Models\ProductOptionValue;
|
use Lunar\Models\ProductOptionValue;
|
||||||
use Modules\Core\MigrateImport\DefaultLocale;
|
use Modules\Core\MigrateImport\ImportLocale;
|
||||||
|
|
||||||
class ProductOptionResolver
|
class ProductOptionResolver
|
||||||
{
|
{
|
||||||
@@ -24,8 +24,8 @@ class ProductOptionResolver
|
|||||||
return ProductOption::query()->firstOrCreate(
|
return ProductOption::query()->firstOrCreate(
|
||||||
['handle' => $handle],
|
['handle' => $handle],
|
||||||
[
|
[
|
||||||
'name' => [DefaultLocale::code() => $name],
|
'name' => [ImportLocale::code() => $name],
|
||||||
'label' => [DefaultLocale::code() => $name],
|
'label' => [ImportLocale::code() => $name],
|
||||||
'shared' => true,
|
'shared' => true,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -46,7 +46,7 @@ class ProductOptionResolver
|
|||||||
|
|
||||||
return $existing ?? ProductOptionValue::create([
|
return $existing ?? ProductOptionValue::create([
|
||||||
'product_option_id' => $option->id,
|
'product_option_id' => $option->id,
|
||||||
'name' => [DefaultLocale::code() => $value],
|
'name' => [ImportLocale::code() => $value],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Lunar\Models\Language;
|
|||||||
use Lunar\Models\Product;
|
use Lunar\Models\Product;
|
||||||
use Lunar\Models\ProductVariant;
|
use Lunar\Models\ProductVariant;
|
||||||
use Lunar\Models\Url;
|
use Lunar\Models\Url;
|
||||||
|
use Modules\Core\MigrateImport\ImportLocale;
|
||||||
use Modules\Core\MigrateImport\ImportSpec;
|
use Modules\Core\MigrateImport\ImportSpec;
|
||||||
use Modules\Core\MigrateImport\Importer;
|
use Modules\Core\MigrateImport\Importer;
|
||||||
use Modules\Core\MigrateImport\Models\ImportMapping;
|
use Modules\Core\MigrateImport\Models\ImportMapping;
|
||||||
@@ -48,16 +49,26 @@ class ShopifyExportImporter implements Importer
|
|||||||
|
|
||||||
public function import(ImportSpec $spec): void
|
public function import(ImportSpec $spec): void
|
||||||
{
|
{
|
||||||
$groups = $this->csvReader->read($spec->filePath);
|
if ($spec->locale !== null) {
|
||||||
$imagesPath = dirname($spec->filePath).'/files';
|
ImportLocale::set($spec->locale);
|
||||||
$collectionGroup = CollectionGroup::firstOrCreate(
|
}
|
||||||
['handle' => 'shopify'],
|
|
||||||
['name' => 'Shopify'],
|
|
||||||
);
|
|
||||||
$currency = Currency::getDefault();
|
|
||||||
|
|
||||||
foreach ($groups as $group) {
|
try {
|
||||||
$this->importProduct($group, $imagesPath, $collectionGroup, $currency);
|
$groups = $this->csvReader->read($spec->filePath);
|
||||||
|
$imagesPath = dirname($spec->filePath).'/files';
|
||||||
|
$collectionGroup = CollectionGroup::firstOrCreate(
|
||||||
|
['handle' => 'shopify'],
|
||||||
|
['name' => 'Shopify'],
|
||||||
|
);
|
||||||
|
$currency = Currency::getDefault();
|
||||||
|
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
$this->importProduct($group, $imagesPath, $collectionGroup, $currency);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// A queue worker process outlives a single import run — this
|
||||||
|
// must not leak into whatever's imported next.
|
||||||
|
ImportLocale::reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +111,12 @@ class ShopifyExportImporter implements Importer
|
|||||||
[
|
[
|
||||||
'element_type' => $product->getMorphClass(),
|
'element_type' => $product->getMorphClass(),
|
||||||
'element_id' => $product->id,
|
'element_id' => $product->id,
|
||||||
'language_id' => Language::getDefault()->id,
|
// ImportLocale, not Language::getDefault() — same
|
||||||
|
// reasoning as ProductAttributeResolver et al.: this
|
||||||
|
// product's handle/slug came from the export, written in
|
||||||
|
// whatever language the operator said the file is in,
|
||||||
|
// not necessarily this store's own default language.
|
||||||
|
'language_id' => Language::where('code', ImportLocale::code())->value('id'),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'slug' => $group->handle,
|
'slug' => $group->handle,
|
||||||
|
|||||||
Reference in New Issue
Block a user