A add example products command, I cleanup

This commit is contained in:
a6a2f5842 2025-11-21 15:52:06 +01:00
parent 8fa194e7e6
commit ce41dea486
6 changed files with 433 additions and 141 deletions

View File

@ -141,6 +141,31 @@ return [
## Commands
### Add Example Products
Create example products for testing and demonstration purposes:
```bash
# Create 2 products of each type (default)
php artisan shop:add-example-products
# Create 5 products of each type
php artisan shop:add-example-products --count=5
# Clean existing example products first
php artisan shop:add-example-products --clean
```
This command creates:
- ✅ All 4 product types (simple, variable, grouped, external)
- ✅ Product categories
- ✅ Product attributes (material, size, color, etc.)
- ✅ Multiple pricing options (multi-currency, subscriptions)
- ✅ Example product actions (email notifications, stats updates)
- ✅ Variations for variable products
- ✅ Child products for grouped products
- ✅ Realistic data using Faker
### Reinstall Shop Tables
```bash

View File

@ -0,0 +1,407 @@
<?php
namespace Blax\Shop\Console\Commands;
use Blax\Shop\Models\Product;
use Blax\Shop\Models\ProductAction;
use Blax\Shop\Models\ProductAttribute;
use Blax\Shop\Models\ProductCategory;
use Blax\Shop\Models\ProductPrice;
use Illuminate\Console\Command;
use Faker\Factory as Faker;
class ShopAddExampleProducts extends Command
{
protected $signature = 'shop:add-example-products
{--clean : Remove existing example products first}
{--count=2 : Number of products per type}';
protected $description = 'Adds all possible example products to the shop for demonstration purposes.';
/**
* Available product types in the shop system
*/
const PRODUCT_TYPES = [
'simple' => [
'name' => 'Simple Product',
'description' => 'A standalone product with no variations (e.g., a book, a service)',
],
'variable' => [
'name' => 'Variable Product',
'description' => 'A product with variations/options (e.g., a t-shirt with different sizes and colors)',
],
'grouped' => [
'name' => 'Grouped Product',
'description' => 'A collection of related products sold together (e.g., a product bundle)',
],
'external' => [
'name' => 'External Product',
'description' => 'A product that links to an external site for purchase',
],
];
protected $faker;
protected $categories = [];
public function handle()
{
$this->faker = Faker::create();
if ($this->option('clean')) {
$this->cleanExampleProducts();
}
$this->info('Creating example products for Laravel Shop Package...');
$this->newLine();
// Create categories first
$this->createCategories();
$count = (int) $this->option('count');
$totalCreated = 0;
foreach (self::PRODUCT_TYPES as $type => $details) {
$this->line("<fg=cyan>Creating {$count} {$details['name']}(s)...</>");
for ($i = 1; $i <= $count; $i++) {
$product = $this->createProduct($type, $i);
$totalCreated++;
$this->line(" <fg=green>✓</> {$product->slug}");
}
$this->newLine();
}
$this->info("✓ Successfully created {$totalCreated} example products!");
$this->line(" - Products: {$totalCreated}");
$this->line(" - Categories: " . count($this->categories));
$this->newLine();
$this->info("You can view them in your shop or use them for testing.");
}
protected function cleanExampleProducts(): void
{
$this->warn('Cleaning existing example products...');
Product::where('slug', 'like', 'example-%')->delete();
ProductCategory::where('slug', 'like', 'example-%')->delete();
$this->info('✓ Cleaned existing example products');
$this->newLine();
}
protected function createCategories(): void
{
$categoryNames = [
'Electronics' => 'Electronic devices and gadgets',
'Clothing' => 'Apparel and fashion items',
'Books' => 'Books and publications',
'Home & Garden' => 'Home improvement and garden supplies',
'Sports' => 'Sports equipment and accessories',
];
foreach ($categoryNames as $name => $description) {
$category = ProductCategory::firstOrCreate(
['slug' => 'example-' . \Illuminate\Support\Str::slug($name)],
[
'name' => $name,
'description' => $description,
'visible' => true,
'sort_order' => 0,
'meta' => json_encode((object)[]),
]
);
$this->categories[] = $category;
}
}
protected function createProduct(string $type, int $index): Product
{
$productName = $this->generateProductName($type);
$slug = 'example-' . \Illuminate\Support\Str::slug($productName) . '-' . $this->faker->unique()->numberBetween(1000, 9999);
$regularPrice = $this->faker->randomFloat(2, 10, 500);
$onSale = $this->faker->boolean(30); // 30% chance of being on sale
$product = Product::create([
'slug' => $slug,
'sku' => 'EX-' . strtoupper($this->faker->bothify('??-####')),
'type' => $type,
'status' => $this->faker->randomElement(['published', 'published', 'published', 'draft']), // mostly published
'visible' => true,
'featured' => $this->faker->boolean(20), // 20% featured
'price' => $onSale ? $regularPrice * 0.8 : $regularPrice,
'regular_price' => $regularPrice,
'sale_price' => $onSale ? $regularPrice * $this->faker->randomFloat(2, 0.6, 0.9) : null,
'sale_start' => $onSale ? now()->subDays($this->faker->numberBetween(1, 30)) : null,
'sale_end' => $onSale ? now()->addDays($this->faker->numberBetween(7, 60)) : null,
'manage_stock' => $type !== 'external',
'stock_quantity' => $type !== 'external' ? $this->faker->numberBetween(0, 100) : 0,
'low_stock_threshold' => $type !== 'external' ? 5 : null,
'in_stock' => true,
'stock_status' => 'instock',
'weight' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 0.1, 50),
'length' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
'width' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
'height' => $type === 'virtual' ? null : $this->faker->randomFloat(2, 5, 100),
'virtual' => $type === 'variable' ? $this->faker->boolean(20) : false,
'downloadable' => $type === 'simple' ? $this->faker->boolean(15) : false,
'published_at' => now(),
'sort_order' => $this->faker->numberBetween(0, 100),
'tax_class' => $this->faker->randomElement(['standard', 'reduced', 'zero']),
'meta' => json_encode((object)[
'description' => $this->faker->paragraph(3),
'short_description' => $this->faker->sentence(10),
'example' => true,
]),
]);
// Set localized name
$product->setLocalized('name', $productName, null, true);
// Add to random categories
$randomCategories = $this->faker->randomElements($this->categories, $this->faker->numberBetween(1, 3));
$product->categories()->attach(collect($randomCategories)->pluck('id'));
// Add attributes
$this->addAttributes($product, $type);
// Add additional prices (multi-currency or subscription)
if ($type === 'simple' || $type === 'variable') {
$this->addAdditionalPrices($product);
}
// Add example actions
$this->addExampleActions($product);
// For variable products, add variations
if ($type === 'variable') {
$this->addVariations($product);
}
// For grouped products, add child products
if ($type === 'grouped') {
$this->addGroupedProducts($product);
}
return $product;
}
protected function generateProductName(string $type): string
{
$names = [
'simple' => [
'Premium Wireless Headphones',
'Organic Cotton T-Shirt',
'Stainless Steel Water Bottle',
'Leather Wallet',
'Bamboo Cutting Board',
'Yoga Mat Pro',
'Coffee Mug Set',
'Digital Course: Web Development',
],
'variable' => [
'Classic Running Shoes',
'Designer Hoodie',
'Smart Watch Ultra',
'Backpack Collection',
'Sunglasses Elite',
'Fitness Tracker Band',
],
'grouped' => [
'Home Office Starter Kit',
'Camping Essentials Bundle',
'Kitchen Utensil Set',
'Travel Accessories Pack',
'Gaming Setup Bundle',
],
'external' => [
'External Brand Laptop',
'Partner Store Gift Card',
'Affiliate Product Link',
'Third-Party Service',
],
];
return $this->faker->randomElement($names[$type] ?? $names['simple']);
}
protected function addAttributes(Product $product, string $type): void
{
$attributes = [];
switch ($type) {
case 'simple':
$attributes = [
['name' => 'Material', 'value' => $this->faker->randomElement(['Cotton', 'Polyester', 'Leather', 'Metal', 'Plastic', 'Wood'])],
['name' => 'Brand', 'value' => $this->faker->company()],
['name' => 'Country of Origin', 'value' => $this->faker->country()],
];
break;
case 'variable':
$attributes = [
['name' => 'Size', 'value' => $this->faker->randomElement(['S, M, L, XL', 'One Size', '6-12'])],
['name' => 'Color', 'value' => $this->faker->randomElement(['Red, Blue, Green', 'Black, White', 'Multi-Color'])],
['name' => 'Material', 'value' => $this->faker->randomElement(['Cotton', 'Polyester', 'Blend'])],
];
break;
case 'grouped':
$attributes = [
['name' => 'Items Included', 'value' => $this->faker->numberBetween(3, 10) . ' pieces'],
['name' => 'Bundle Type', 'value' => 'Curated Collection'],
];
break;
case 'external':
$attributes = [
['name' => 'External URL', 'value' => 'https://example.com/product'],
['name' => 'Affiliate Link', 'value' => 'Yes'],
];
break;
}
foreach ($attributes as $index => $attr) {
ProductAttribute::create([
'product_id' => $product->id,
'name' => $attr['name'],
'value' => $attr['value'],
'sort_order' => $index,
'meta' => json_encode((object)[]),
]);
}
}
protected function addAdditionalPrices(Product $product): void
{
// Add a subscription price option
if ($this->faker->boolean(30)) {
ProductPrice::create([
'product_id' => $product->id,
'active' => true,
'name' => 'Monthly Subscription',
'type' => 'recurring',
'price' => (int)($product->price * 100 * 0.3), // 30% of regular price monthly
'billing_scheme' => 'per_unit',
'interval' => 'month',
'interval_count' => 1,
'trial_period_days' => $this->faker->randomElement([0, 7, 14, 30]),
'currency' => 'usd',
'is_default' => false,
'meta' => json_encode((object)['example' => true]),
]);
}
// Add EUR price
if ($this->faker->boolean(40)) {
ProductPrice::create([
'product_id' => $product->id,
'active' => true,
'name' => 'EUR Price',
'type' => 'one_time',
'price' => (int)($product->price * 100 * 0.92), // Convert to cents and EUR rate
'billing_scheme' => 'per_unit',
'currency' => 'eur',
'is_default' => false,
'meta' => json_encode((object)['example' => true]),
]);
}
}
protected function addExampleActions(Product $product): void
{
$actions = [
[
'event' => 'purchased',
'action_type' => 'SendThankYouEmail',
'config' => ['template' => 'thank-you', 'delay' => 0],
'description' => 'Send thank you email after purchase',
],
[
'event' => 'purchased',
'action_type' => 'UpdateCustomerStats',
'config' => ['increment' => 'total_purchases'],
'description' => 'Update customer purchase statistics',
],
[
'event' => 'low_stock',
'action_type' => 'NotifyAdmin',
'config' => ['threshold' => 5],
'description' => 'Notify admin when stock is low',
],
];
foreach ($actions as $index => $actionData) {
ProductAction::create([
'product_id' => $product->id,
'event' => $actionData['event'],
'action_type' => $actionData['action_type'],
'config' => $actionData['config'],
'active' => $this->faker->boolean(70), // 70% active
'sort_order' => $index,
]);
}
}
protected function addVariations(Product $product): void
{
$variations = ['Small', 'Medium', 'Large'];
foreach ($variations as $index => $variation) {
$variationProduct = Product::create([
'slug' => $product->slug . '-' . \Illuminate\Support\Str::slug($variation),
'sku' => $product->sku . '-' . strtoupper(substr($variation, 0, 1)),
'type' => 'simple',
'parent_id' => $product->id,
'status' => 'published',
'visible' => false, // Variations are not directly visible
'price' => $product->price + ($index * 5), // Slight price increase per size
'regular_price' => $product->regular_price + ($index * 5),
'manage_stock' => true,
'stock_quantity' => $this->faker->numberBetween(5, 50),
'in_stock' => true,
'stock_status' => 'instock',
'published_at' => now(),
'meta' => json_encode((object)['variation' => $variation, 'example' => true]),
]);
$variationProduct->setLocalized('name', $product->name . ' - ' . $variation, null, true);
ProductAttribute::create([
'product_id' => $variationProduct->id,
'name' => 'Size',
'value' => $variation,
'sort_order' => 0,
'meta' => json_encode((object)[]),
]);
}
}
protected function addGroupedProducts(Product $product): void
{
$groupSize = $this->faker->numberBetween(2, 4);
for ($i = 0; $i < $groupSize; $i++) {
$childProduct = Product::create([
'slug' => $product->slug . '-item-' . ($i + 1),
'sku' => $product->sku . '-' . ($i + 1),
'type' => 'simple',
'parent_id' => $product->id,
'status' => 'published',
'visible' => false,
'price' => $this->faker->randomFloat(2, 10, 100),
'manage_stock' => true,
'stock_quantity' => $this->faker->numberBetween(10, 50),
'in_stock' => true,
'stock_status' => 'instock',
'published_at' => now(),
'meta' => json_encode((object)['grouped_item' => true, 'example' => true]),
]);
$childProduct->setLocalized('name', $this->faker->words(3, true), null, true);
}
}
}

View File

@ -1,47 +0,0 @@
<?php
namespace Blax\Shop\Console\Commands;
use Blax\Shop\Models\ProductAction;
use Illuminate\Console\Command;
class ShopAvailableActionsCommand extends Command
{
protected $signature = 'shop:available-actions';
protected $description = 'List all available action classes that can be used';
public function handle()
{
$actions = ProductAction::getAvailableActions();
if (empty($actions)) {
$this->warn('No action classes found.');
$this->info('Make sure auto_discover is enabled in config/shop.php');
$this->info('Path: ' . config('shop.actions.path', app_path('Jobs/ProductAction')));
return 0;
}
$this->info('Available Action Classes:');
$this->newLine();
foreach ($actions as $className => $parameters) {
$this->line("• <fg=green>{$className}</>");
if (!empty($parameters)) {
$this->line(' Parameters:');
foreach ($parameters as $param => $description) {
$this->line(" - {$param}: {$description}");
}
} else {
$this->line(' No parameters');
}
$this->newLine();
}
$this->info("Total: " . count($actions) . " action class(es)");
return 0;
}
}

View File

@ -1,62 +0,0 @@
<?php
namespace Blax\Shop\Console\Commands;
use Blax\Shop\Models\ProductAction;
use Illuminate\Console\Command;
class ShopListActionsCommand extends Command
{
protected $signature = 'shop:list-actions
{product? : Product ID to filter by}
{--event= : Filter by event type}
{--enabled : Only show enabled actions}
{--disabled : Only show disabled actions}';
protected $description = 'List all product actions';
public function handle()
{
$query = ProductAction::with('product');
if ($productId = $this->argument('product')) {
$query->where('product_id', $productId);
}
if ($event = $this->option('event')) {
$query->where('event', $event);
}
if ($this->option('enabled')) {
$query->where('enabled', true);
} elseif ($this->option('disabled')) {
$query->where('enabled', false);
}
$actions = $query->orderBy('product_id')->orderBy('priority')->get();
if ($actions->isEmpty()) {
$this->info('No actions found.');
return 0;
}
$headers = ['ID', 'Product', 'Event', 'Action Class', 'Priority', 'Enabled', 'Parameters'];
$rows = $actions->map(function ($action) {
return [
$action->id,
$action->product->name ?? "ID: {$action->product_id}",
$action->event,
$action->action_class,
$action->priority,
$action->enabled ? '✓' : '✗',
json_encode($action->parameters),
];
});
$this->table($headers, $rows);
$this->info("Total actions: {$actions->count()}");
return 0;
}
}

View File

@ -37,36 +37,6 @@ class ProductAction extends Model
return $this->belongsTo(config('shop.models.product', Product::class));
}
public static function getAvailableActions(): array
{
if (!config('shop.actions.auto_discover')) {
return [];
}
$path = config('shop.actions.path', app_path('Jobs/ProductAction'));
$namespace = config('shop.actions.namespace', 'App\\Jobs\\ProductAction');
if (!file_exists($path)) {
return [];
}
$actions = collect(glob($path . '/*.php'));
$actions = $actions->mapWithKeys(function ($filePath) use ($path, $namespace) {
$className = str_replace(['.php', $path . '/'], '', $filePath);
$class = $namespace . '\\' . $className;
if (!class_exists($class) || !method_exists($class, 'parameters')) {
return [];
}
$params = $class::parameters();
return [$className => $params];
});
return $actions->toArray();
}
public static function callForProduct(
Product $product,
string $event,

View File

@ -38,12 +38,11 @@ class ShopServiceProvider extends ServiceProvider
ShopReinstallCommand::class,
\Blax\Shop\Console\Commands\ReleaseExpiredStocks::class,
\Blax\Shop\Console\Commands\ShopListProductsCommand::class,
\Blax\Shop\Console\Commands\ShopListActionsCommand::class,
\Blax\Shop\Console\Commands\ShopToggleActionCommand::class,
\Blax\Shop\Console\Commands\ShopTestActionCommand::class,
\Blax\Shop\Console\Commands\ShopListPurchasesCommand::class,
\Blax\Shop\Console\Commands\ShopAvailableActionsCommand::class,
\Blax\Shop\Console\Commands\ShopStatsCommand::class,
\Blax\Shop\Console\Commands\ShopAddExampleProducts::class,
]);
}
}