থিম
অধ্যায় ৯: প্যাকেজ কনসোল কমান্ডস এবং অটোমেশন আর্কিটেকচার
প্যাকেজের কার্যকারিতা ইউজারের কাছে পৌঁছানোর আরেকটি গুরুত্বপূর্ণ উপায় হলো কনসোল কমান্ডস। একটি ভালো প্যাকেজে কমপক্ষে ২টি কমান্ড থাকা উচিত: ১. Install Command (সেটআপ অটোমেশন), ২. Maintenance Command (ডাটা ক্লিনআপ)। এই অধ্যায়ে আমরা দেখব কীভাবে প্রফেশনাল লেভেলের কমান্ড আর্কিটেকচার ডিজাইন করতে হয়।
৯.১ কমান্ড স্ট্রাকচার এবং ডিজাইন প্যাটার্ন
প্যাকেজের কমান্ডগুলো আমরা src/Console/Commands ফোল্ডারে রাখব।
ধরি, আমরা একটি কমান্ড বানাব যা ড্রাফট ইনভয়েসগুলো ক্লিন করবে যা ৩০ দিনের বেশি পুরনো। নাম: invoice:prune।
ফাইল: src/Console/Commands/PruneInvoicesCommand.php
php
namespace DevMaster\InvoiceLite\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceLite\Models\Invoice;
class PruneInvoicesCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'invoicelite:prune
{--days=30 : The number of days to retain invoices}
{--force : Force the operation to run when in production}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune old draft invoices from the database';
/**
* Execute the console command.
*/
public function handle(): void
{
$days = $this->option('days');
// প্রোডাকশন সেফটি চেক
if (app()->environment() === 'production' && ! $this->option('force')) {
if (! $this->confirm("Do you wish to delete invoices older than {$days} days?")) {
$this->info('Operation cancelled.');
return;
}
}
$this->info("Pruning invoices older than {$days} days...");
// লজিক
$count = Invoice::where('status', 'draft')
->where('created_at', '<', now()->subDays($days))
->delete();
$this->info("Deleted {$count} old draft invoices.");
}
}৯.২ কমান্ড রেজিস্ট্রেশন: সার্ভিস প্রভাইডার ইন্টারনালস
সাধারণ লারাভেল অ্যাপে কমান্ডগুলো অটোমেটিক লোড হয়। কিন্তু প্যাকেজে আমাদের ম্যানুয়ালি রেজিস্টার করতে হয়। এটি সার্ভিস প্রভাইডারের boot মেথডে করতে হয়।
ফাইল: src/InvoiceLiteServiceProvider.php
php
use DevMaster\InvoiceLite\Console\Commands\PruneInvoicesCommand;
use DevMaster\InvoiceLite\Console\Commands\InstallCommand;
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
InstallCommand::class,
PruneInvoicesCommand::class,
]);
}
}৯.২.১ runningInConsole() এর মেমরি ইমপ্যাক্ট
আমরা কেন এই চেকটি দিই? প্রতিটি HTTP রিকোয়েস্টে (যেমন ওয়েবসাইট লোড হওয়ার সময়) আমাদের কনসোল কমান্ডগুলো লোড করার কোনো প্রয়োজন নেই। এটি মেমরির অপচয়। runningInConsole() চেক করে নিশ্চিত করা হয় যে, শুধুমাত্র যখন ইউজার টার্মিনালে php artisan চালাবে, তখনই এই ক্লাসগুলো মেমরিতে লোড হবে।
৯.৩ ইনস্টলেশন কমান্ড: দ্য অনবোর্ডিং এক্সপেরিয়েন্স
একটি ভালো প্যাকেজের লক্ষণ হলো এর সেটআপ সহজ হওয়া। ইউজারকে ম্যানুয়ালি vendor:publish এবং migrate রান করতে বলা বিরক্তিকর। আমরা সব কাজ একটি invoicelite:install কমান্ডের মাধ্যমে অটোমেট করব।
ফাইল: src/Console/Commands/InstallCommand.php
php
namespace DevMaster\InvoiceLite\Console\Commands;
use Illuminate\Console\Command;
class InstallCommand extends Command
{
protected $signature = 'invoicelite:install';
protected $description = 'Install InvoiceLite Package';
public function handle(): void
{
$this->info('Installing InvoiceLite...');
// ১. কনফিগ পাবলিশ করা
$this->call('vendor:publish', [
'--tag' => 'invoicelite-config',
]);
// ২. মাইগ্রেশন রান করা (অপশনাল - ইউজারকে জিজ্ঞেস করা উচিত)
if ($this->confirm('Do you want to run the migrations now?', true)) {
$this->call('migrate');
}
// ৩. সিডিং বা অন্য সেটআপ
$this->call('db:seed', ['--class' => 'DevMaster\\InvoiceLite\\Database\\Seeders\\InvoiceSeeder']);
$this->info('InvoiceLite installed successfully!');
}
}৯.৩.১ call() বনাম callSilently()
- call(): সাব-কমান্ডের আউটপুট ইউজার দেখতে পাবে।
- callSilently(): আউটপুট লুকিয়ে রাখবে, শুধু রেজাল্ট রিটার্ন করবে।
ইনস্টলেশনের সময় সাধারণত call() ব্যবহার করা ভালো যাতে ইউজার বুঝতে পারে কী ঘটছে।
৯.৪ শিডিউলিং ইনজেকশন
সবচেয়ে চ্যালেঞ্জিং এবং ইন্টারেস্টিং পার্ট। সাধারণত ইউজারকে বলা হয়: "আপনার app/Console/Kernel.php ফাইলে গিয়ে এই লাইনটি যোগ করুন।"
php
$schedule->command('invoicelite:prune')->daily();কিন্তু একজন প্যাকেজ আর্কিটেক্ট হিসেবে আমরা চাই ইউজারকে এই ঝামেলা থেকে মুক্তি দিতে। আমরা প্যাকেজের ভেতর থেকেই শিডিউলার হুক (Hook) করব।
৯.৪.১ resolving ইভেন্ট ব্যবহার করা
লারাভেলের সার্ভিস কন্টেইনার যখন Illuminate\Console\Scheduling\Schedule ক্লাসটি রিজলভ করে, তখন আমরা আমাদের কমান্ড সেখানে পুশ করে দেব।
Service Provider Update:
php
use Illuminate\Console\Scheduling\Schedule;
public function boot(): void
{
// কমান্ড রেজিস্ট্রেশন...
$this->commands([PruneInvoicesCommand::class]);
// অটো-শিডিউলিং ম্যাজিক
$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
// আমাদের কমান্ড শিডিউলে যুক্ত করা হলো
$schedule->command('invoicelite:prune')->dailyAt('02:00');
});
}সতর্কতা: এই টেকনিকটি ব্যবহারের সময় ইউজারকে ডকুমেন্টেশনে স্পষ্টভাবে জানিয়ে দেবেন যে প্যাকেজটি ব্যাকগ্রাউন্ডে একটি ক্রন জব চালায়। এবং কনফিগারেশনে এটি বন্ধ করার অপশন রাখা উচিত (if (config('invoicelite.auto_schedule')) ...)।
🔨 ৯.৫ Practical Implementation: Console Commands & Automation for InvoiceBuilder
এখন আমরা আমাদের InvoiceBuilder package এ comprehensive console commands এবং automation features যোগ করব।
৯.৫.১ Enhanced Configuration for Commands
প্রথমে Chapter 5 এর configuration এ commands ও automation options যোগ করি:
php
// packages/DevMaster/InvoiceBuilder/config/invoice-builder.php এ commands section যোগ করুন
/*
|--------------------------------------------------------------------------
| Console Commands Configuration
|--------------------------------------------------------------------------
*/
'commands' => [
'enabled' => env('INVOICE_COMMANDS_ENABLED', true),
// Installation command settings
'install' => [
'auto_migrate' => env('INVOICE_AUTO_MIGRATE', false),
'auto_seed' => env('INVOICE_AUTO_SEED', false),
'publish_assets' => env('INVOICE_PUBLISH_ASSETS', true),
],
// Maintenance commands
'maintenance' => [
'prune_enabled' => env('INVOICE_PRUNE_ENABLED', true),
'default_prune_days' => env('INVOICE_PRUNE_DAYS', 30),
'backup_before_prune' => env('INVOICE_BACKUP_BEFORE_PRUNE', true),
],
// Automation settings
'scheduling' => [
'enabled' => env('INVOICE_SCHEDULING_ENABLED', false),
'prune_schedule' => env('INVOICE_PRUNE_SCHEDULE', 'daily'),
'prune_time' => env('INVOICE_PRUNE_TIME', '02:00'),
'overdue_check_schedule' => env('INVOICE_OVERDUE_SCHEDULE', 'hourly'),
'cleanup_temp_files' => env('INVOICE_CLEANUP_TEMP', 'weekly'),
],
// Notification settings for commands
'notifications' => [
'enabled' => env('INVOICE_COMMAND_NOTIFICATIONS', true),
'channels' => ['mail'], // mail, slack, teams
'recipients' => env('INVOICE_COMMAND_NOTIFICATION_EMAILS', ''),
],
],
/*
|--------------------------------------------------------------------------
| Reports & Analytics
|--------------------------------------------------------------------------
*/
'reports' => [
'enabled' => env('INVOICE_REPORTS_ENABLED', true),
'default_format' => env('INVOICE_REPORTS_FORMAT', 'table'), // table, csv, json
'export_path' => env('INVOICE_REPORTS_PATH', 'storage/app/reports'),
],৯.৫.২ Installation Command with Progress Tracking
php
// packages/DevMaster/InvoiceBuilder/src/Console/Commands/InstallCommand.php
<?php
namespace DevMaster\InvoiceBuilder\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceBuilder\Services\ConfigManager;
class InstallCommand extends Command
{
protected $signature = 'invoice-builder:install
{--force : Force installation even if already installed}
{--no-migrate : Skip running migrations}
{--no-seed : Skip running seeders}
{--no-publish : Skip publishing assets}';
protected $description = 'Install InvoiceBuilder package with all necessary setup';
protected ConfigManager $config;
public function __construct(ConfigManager $config)
{
parent::__construct();
$this->config = $config;
}
public function handle(): int
{
$this->displayHeader();
// Check if already installed
if ($this->isAlreadyInstalled() && !$this->option('force')) {
$this->error('InvoiceBuilder is already installed!');
$this->line('Use --force to reinstall or run specific setup commands.');
return self::FAILURE;
}
$steps = $this->getInstallationSteps();
$progressBar = $this->output->createProgressBar(count($steps));
$progressBar->start();
foreach ($steps as $step => $description) {
$this->line("\n<info>$description</info>");
try {
$this->executeInstallationStep($step);
$progressBar->advance();
} catch (\Exception $e) {
$progressBar->finish();
$this->line('');
$this->error("Installation failed at step: $description");
$this->error("Error: " . $e->getMessage());
return self::FAILURE;
}
}
$progressBar->finish();
$this->line('');
$this->displaySuccess();
return self::SUCCESS;
}
protected function displayHeader(): void
{
$this->line('');
$this->line('<fg=blue>╔══════════════════════════════════════════╗</fg=blue>');
$this->line('<fg=blue>║ InvoiceBuilder Setup ║</fg=blue>');
$this->line('<fg=blue>╚══════════════════════════════════════════╝</fg=blue>');
$this->line('');
}
protected function isAlreadyInstalled(): bool
{
return file_exists(config_path('invoice-builder.php'));
}
protected function getInstallationSteps(): array
{
$steps = [
'check_requirements' => 'Checking system requirements',
'publish_config' => 'Publishing configuration files',
];
if (!$this->option('no-publish')) {
$steps['publish_assets'] = 'Publishing assets and views';
}
if (!$this->option('no-migrate')) {
$steps['migrate'] => 'Running database migrations';
}
if (!$this->option('no-seed')) {
$steps['seed'] => 'Seeding initial data';
}
$steps['finalize'] = 'Finalizing installation';
return $steps;
}
protected function executeInstallationStep(string $step): void
{
match($step) {
'check_requirements' => $this->checkRequirements(),
'publish_config' => $this->publishConfiguration(),
'publish_assets' => $this->publishAssets(),
'migrate' => $this->runMigrations(),
'seed' => $this->runSeeders(),
'finalize' => $this->finalizeInstallation(),
};
}
protected function checkRequirements(): void
{
$requirements = [
'PHP >= 8.1' => version_compare(PHP_VERSION, '8.1.0', '>='),
'Laravel >= 10.0' => version_compare(app()->version(), '10.0', '>='),
'mPDF Extension' => class_exists('Mpdf\Mpdf'),
'GD Extension' => extension_loaded('gd'),
];
$failed = [];
foreach ($requirements as $requirement => $check) {
if (!$check) {
$failed[] = $requirement;
}
}
if (!empty($failed)) {
throw new \Exception('Missing requirements: ' . implode(', ', $failed));
}
}
protected function publishConfiguration(): void
{
$this->callSilently('vendor:publish', [
'--tag' => 'invoice-builder-config',
'--force' => $this->option('force'),
]);
}
protected function publishAssets(): void
{
$this->callSilently('vendor:publish', [
'--tag' => 'invoice-builder-views',
'--force' => $this->option('force'),
]);
$this->callSilently('vendor:publish', [
'--tag' => 'invoice-builder-assets',
'--force' => $this->option('force'),
]);
}
protected function runMigrations(): void
{
if ($this->config->get('database.migration_strategy', 'publish') === 'publish') {
$this->callSilently('vendor:publish', [
'--tag' => 'invoice-builder-migrations',
'--force' => $this->option('force'),
]);
}
$this->call('migrate');
}
protected function runSeeders(): void
{
if ($this->config->get('commands.install.auto_seed', false)) {
$this->call('db:seed', [
'--class' => 'DevMaster\\InvoiceBuilder\\Database\\Seeders\\InvoiceBuilderSeeder'
]);
}
}
protected function finalizeInstallation(): void
{
// Create necessary directories
$directories = [
storage_path('app/invoices'),
storage_path('app/invoice-templates'),
public_path('vendor/invoice-builder'),
];
foreach ($directories as $directory) {
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
}
// Set initial configuration
$this->setInitialConfiguration();
}
protected function setInitialConfiguration(): void
{
$configPath = config_path('invoice-builder.php');
if (file_exists($configPath)) {
// Add any environment-specific configurations
$this->line(' <comment>→ Configuration file is ready for customization</comment>');
}
}
protected function displaySuccess(): void
{
$this->line('');
$this->line('<fg=green>✓ Installation completed successfully!</fg=green>');
$this->line('');
$this->line('<comment>Next steps:</comment>');
$this->line(' 1. Review configuration: <info>config/invoice-builder.php</info>');
$this->line(' 2. Configure your .env file with INVOICE_* variables');
$this->line(' 3. Run: <info>php artisan invoice-builder:status</info> to verify setup');
$this->line('');
$this->line('<comment>Documentation:</comment> Visit package documentation for usage examples');
$this->line('');
}
}৯.৫.৩ Maintenance and Cleanup Commands
Prune Command:
php
// packages/DevMaster/InvoiceBuilder/src/Console/Commands/PruneInvoicesCommand.php
<?php
namespace DevMaster\InvoiceBuilder\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceBuilder\Models\Invoice;
use DevMaster\InvoiceBuilder\Services\ConfigManager;
class PruneInvoicesCommand extends Command
{
protected $signature = 'invoice-builder:prune
{--days= : Number of days to keep invoices}
{--status=draft : Invoice status to prune}
{--force : Force operation without confirmation}
{--dry-run : Show what would be deleted without deleting}';
protected $description = 'Prune old invoices from the database';
protected ConfigManager $config;
public function __construct(ConfigManager $config)
{
parent::__construct();
$this->config = $config;
}
public function handle(): int
{
$days = $this->option('days') ?? $this->config->get('commands.maintenance.default_prune_days', 30);
$status = $this->option('status');
$isDryRun = $this->option('dry-run');
// Safety checks
if (!$this->config->get('commands.maintenance.prune_enabled', true)) {
$this->error('Pruning is disabled in configuration.');
return self::FAILURE;
}
if ($days < 7) {
$this->error('Cannot prune invoices less than 7 days old for safety.');
return self::FAILURE;
}
// Get invoices to prune
$query = Invoice::where('status', $status)
->where('created_at', '<', now()->subDays($days));
$count = $query->count();
if ($count === 0) {
$this->info("No invoices found to prune (status: {$status}, older than {$days} days).");
return self::SUCCESS;
}
// Display what will be pruned
$this->line("Found <comment>{$count}</comment> invoices to prune:");
$this->line(" • Status: <info>{$status}</info>");
$this->line(" • Older than: <info>{$days} days</info>");
if ($isDryRun) {
$this->displayPrunePreview($query);
$this->line('');
$this->line('<comment>This was a dry run. No invoices were deleted.</comment>');
return self::SUCCESS;
}
// Confirmation
if (!$this->option('force') && !$this->confirmPrune($count, $status, $days)) {
$this->line('Operation cancelled.');
return self::SUCCESS;
}
// Backup if enabled
if ($this->config->get('commands.maintenance.backup_before_prune', true)) {
$this->backup($query);
}
// Execute pruning
$this->line('');
$progressBar = $this->output->createProgressBar($count);
$progressBar->setMessage('Pruning invoices...');
$deleted = 0;
$query->chunk(100, function ($invoices) use ($progressBar, &$deleted) {
foreach ($invoices as $invoice) {
$invoice->delete();
$deleted++;
$progressBar->advance();
}
});
$progressBar->finish();
$this->line('');
$this->line('');
$this->info("Successfully pruned {$deleted} invoices.");
return self::SUCCESS;
}
protected function displayPrunePreview($query): void
{
$this->line('');
$this->line('<comment>Preview of invoices to be pruned:</comment>');
$this->table(
['ID', 'Invoice Number', 'Status', 'Created', 'Days Old'],
$query->take(10)->get()->map(function ($invoice) {
return [
$invoice->id,
$invoice->invoice_number,
$invoice->status,
$invoice->created_at->format('Y-m-d H:i'),
$invoice->created_at->diffInDays(now()),
];
})->toArray()
);
if ($query->count() > 10) {
$this->line("<comment>... and " . ($query->count() - 10) . " more invoices</comment>");
}
}
protected function confirmPrune(int $count, string $status, int $days): bool
{
if (app()->environment('production')) {
$this->warn('You are running this in PRODUCTION environment!');
}
return $this->confirm(
"Are you sure you want to delete {$count} {$status} invoices older than {$days} days?"
);
}
protected function backup($query): void
{
$this->line('Creating backup before pruning...');
$backupPath = storage_path('app/backups/invoices_' . now()->format('Y_m_d_His') . '.json');
$backupDir = dirname($backupPath);
if (!is_dir($backupDir)) {
mkdir($backupDir, 0755, true);
}
$data = $query->with('items')->get()->toArray();
file_put_contents($backupPath, json_encode($data, JSON_PRETTY_PRINT));
$this->line(" <comment>→ Backup saved to: {$backupPath}</comment>");
}
}Status Check Command:
php
// packages/DevMaster/InvoiceBuilder/src/Console/Commands/StatusCommand.php
<?php
namespace DevMaster\InvoiceBuilder\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceBuilder\Models\Invoice;
use DevMaster\InvoiceBuilder\Services\ConfigManager;
use Illuminate\Support\Facades\Schema;
class StatusCommand extends Command
{
protected $signature = 'invoice-builder:status
{--detailed : Show detailed information}
{--check-health : Run health checks}';
protected $description = 'Show InvoiceBuilder package status and statistics';
protected ConfigManager $config;
public function __construct(ConfigManager $config)
{
parent::__construct();
$this->config = $config;
}
public function handle(): int
{
$this->displayHeader();
$this->displayBasicInfo();
if ($this->option('detailed')) {
$this->displayDetailedStats();
}
if ($this->option('check-health')) {
$this->runHealthChecks();
}
return self::SUCCESS;
}
protected function displayHeader(): void
{
$this->line('');
$this->line('<fg=blue>InvoiceBuilder Package Status</fg=blue>');
$this->line('<fg=blue>=' . str_repeat('=', 30) . '</fg=blue>');
$this->line('');
}
protected function displayBasicInfo(): void
{
$configExists = file_exists(config_path('invoice-builder.php'));
$tablesExist = $this->checkTablesExist();
$this->line('<comment>Installation Status:</comment>');
$this->line(' Configuration: ' . ($configExists ? '<info>✓ Installed</info>' : '<error>✗ Missing</error>'));
$this->line(' Database: ' . ($tablesExist ? '<info>✓ Migrated</info>' : '<error>✗ Not migrated</error>'));
$this->line('');
if ($tablesExist) {
$stats = $this->getBasicStats();
$this->line('<comment>Basic Statistics:</comment>');
$this->line(" Total Invoices: <info>{$stats['total']}</info>");
$this->line(" Draft: <info>{$stats['draft']}</info>");
$this->line(" Paid: <info>{$stats['paid']}</info>");
$this->line(" Overdue: <info>{$stats['overdue']}</info>");
$this->line('');
}
}
protected function displayDetailedStats(): void
{
if (!$this->checkTablesExist()) {
$this->error('Database tables not found. Run migrations first.');
return;
}
$this->line('<comment>Detailed Statistics:</comment>');
// Status distribution
$statusStats = Invoice::selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status')
->toArray();
$this->table(['Status', 'Count'], collect($statusStats)->map(function ($count, $status) {
return [ucfirst($status), $count];
})->toArray());
// Monthly trends
$monthlyStats = Invoice::selectRaw('DATE_FORMAT(created_at, "%Y-%m") as month, COUNT(*) as count')
->where('created_at', '>=', now()->subYear())
->groupBy('month')
->orderBy('month')
->pluck('count', 'month')
->toArray();
if (!empty($monthlyStats)) {
$this->line('');
$this->line('<comment>Monthly Invoice Creation (Last 12 months):</comment>');
$this->table(['Month', 'Invoices'], collect($monthlyStats)->map(function ($count, $month) {
return [$month, $count];
})->toArray());
}
}
protected function runHealthChecks(): void
{
$this->line('');
$this->line('<comment>Health Checks:</comment>');
$checks = [
'Configuration File' => $this->checkConfigFile(),
'Database Tables' => $this->checkTablesExist(),
'Required Directories' => $this->checkDirectories(),
'PHP Extensions' => $this->checkPHPExtensions(),
'File Permissions' => $this->checkFilePermissions(),
];
foreach ($checks as $check => $status) {
$icon = $status ? '<info>✓</info>' : '<error>✗</error>';
$this->line(" {$icon} {$check}");
}
$this->line('');
$passed = array_sum($checks);
$total = count($checks);
if ($passed === $total) {
$this->info("All health checks passed ({$passed}/{$total})");
} else {
$this->warn("Health checks: {$passed}/{$total} passed");
}
}
protected function getBasicStats(): array
{
return [
'total' => Invoice::count(),
'draft' => Invoice::where('status', 'draft')->count(),
'paid' => Invoice::where('status', 'paid')->count(),
'overdue' => Invoice::overdue()->count(),
];
}
protected function checkTablesExist(): bool
{
$tables = [
$this->config->get('database.tables.invoices', 'invoice_invoices'),
$this->config->get('database.tables.invoice_items', 'invoice_invoice_items'),
];
foreach ($tables as $table) {
if (!Schema::hasTable($table)) {
return false;
}
}
return true;
}
protected function checkConfigFile(): bool
{
return file_exists(config_path('invoice-builder.php'));
}
protected function checkDirectories(): bool
{
$directories = [
storage_path('app/invoices'),
public_path('vendor/invoice-builder'),
];
foreach ($directories as $directory) {
if (!is_dir($directory) || !is_writable($directory)) {
return false;
}
}
return true;
}
protected function checkPHPExtensions(): bool
{
$required = ['gd', 'mbstring', 'json'];
foreach ($required as $extension) {
if (!extension_loaded($extension)) {
return false;
}
}
return true;
}
protected function checkFilePermissions(): bool
{
$paths = [
storage_path('app'),
public_path('vendor'),
];
foreach ($paths as $path) {
if (!is_writable($path)) {
return false;
}
}
return true;
}
}৯.৫.৪ Report Generation Commands
php
// packages/DevMaster/InvoiceBuilder/src/Console/Commands/GenerateReportCommand.php
<?php
namespace DevMaster\InvoiceBuilder\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceBuilder\Models\Invoice;
use DevMaster\InvoiceBuilder\Services\ConfigManager;
class GenerateReportCommand extends Command
{
protected $signature = 'invoice-builder:report
{type : Report type (summary, detailed, overdue)}
{--format=table : Output format (table, csv, json)}
{--period=month : Time period (week, month, quarter, year)}
{--output= : Output file path}';
protected $description = 'Generate invoice reports';
protected ConfigManager $config;
public function __construct(ConfigManager $config)
{
parent::__construct();
$this->config = $config;
}
public function handle(): int
{
$type = $this->argument('type');
$format = $this->option('format');
$period = $this->option('period');
if (!in_array($type, ['summary', 'detailed', 'overdue'])) {
$this->error('Invalid report type. Use: summary, detailed, or overdue');
return self::FAILURE;
}
$this->line("Generating <comment>{$type}</comment> report for <comment>{$period}</comment>...");
$data = $this->generateReportData($type, $period);
if (empty($data)) {
$this->warn('No data found for the specified period.');
return self::SUCCESS;
}
$this->displayReport($data, $format, $type);
if ($this->option('output')) {
$this->saveReport($data, $this->option('output'), $format);
}
return self::SUCCESS;
}
protected function generateReportData(string $type, string $period): array
{
$query = Invoice::query();
// Apply period filter
match($period) {
'week' => $query->where('created_at', '>=', now()->subWeek()),
'month' => $query->where('created_at', '>=', now()->subMonth()),
'quarter' => $query->where('created_at', '>=', now()->subQuarter()),
'year' => $query->where('created_at', '>=', now()->subYear()),
};
return match($type) {
'summary' => $this->generateSummaryData($query),
'detailed' => $this->generateDetailedData($query),
'overdue' => $this->generateOverdueData($query),
};
}
protected function generateSummaryData($query): array
{
$invoices = $query->get();
return [
'total_invoices' => $invoices->count(),
'total_amount' => $invoices->sum('total'),
'paid_amount' => $invoices->where('status', 'paid')->sum('total'),
'pending_amount' => $invoices->whereIn('status', ['pending', 'sent'])->sum('total'),
'overdue_amount' => $invoices->where('status', 'overdue')->sum('total'),
'status_breakdown' => $invoices->groupBy('status')->map->count()->toArray(),
];
}
protected function generateDetailedData($query): array
{
return $query->with('items')->get()->map(function ($invoice) {
return [
'invoice_number' => $invoice->invoice_number,
'customer_name' => $invoice->customer_data['name'] ?? 'N/A',
'status' => $invoice->status,
'total' => $invoice->total,
'created_at' => $invoice->created_at->format('Y-m-d'),
'due_date' => $invoice->due_date->format('Y-m-d'),
'items_count' => $invoice->items->count(),
];
})->toArray();
}
protected function generateOverdueData($query): array
{
return $query->overdue()->get()->map(function ($invoice) {
return [
'invoice_number' => $invoice->invoice_number,
'customer_name' => $invoice->customer_data['name'] ?? 'N/A',
'total' => $invoice->total,
'due_date' => $invoice->due_date->format('Y-m-d'),
'days_overdue' => $invoice->days_overdue,
'created_at' => $invoice->created_at->format('Y-m-d'),
];
})->toArray();
}
protected function displayReport(array $data, string $format, string $type): void
{
match($format) {
'table' => $this->displayTableReport($data, $type),
'csv' => $this->displayCsvReport($data),
'json' => $this->displayJsonReport($data),
};
}
protected function displayTableReport(array $data, string $type): void
{
if ($type === 'summary') {
$this->line('');
$this->line('<comment>Summary Report:</comment>');
$this->table(
['Metric', 'Value'],
[
['Total Invoices', $data['total_invoices']],
['Total Amount', '$' . number_format($data['total_amount'], 2)],
['Paid Amount', '$' . number_format($data['paid_amount'], 2)],
['Pending Amount', '$' . number_format($data['pending_amount'], 2)],
['Overdue Amount', '$' . number_format($data['overdue_amount'], 2)],
]
);
if (!empty($data['status_breakdown'])) {
$this->line('');
$this->line('<comment>Status Breakdown:</comment>');
$this->table(
['Status', 'Count'],
collect($data['status_breakdown'])->map(function ($count, $status) {
return [ucfirst($status), $count];
})->toArray()
);
}
} else {
$headers = array_keys($data[0] ?? []);
$this->table($headers, $data);
}
}
protected function displayCsvReport(array $data): void
{
if (empty($data)) return;
$headers = array_keys($data[0]);
$this->line(implode(',', $headers));
foreach ($data as $row) {
$this->line(implode(',', array_map(function ($value) {
return is_string($value) ? '"' . str_replace('"', '""', $value) . '"' : $value;
}, $row)));
}
}
protected function displayJsonReport(array $data): void
{
$this->line(json_encode($data, JSON_PRETTY_PRINT));
}
protected function saveReport(array $data, string $path, string $format): void
{
$directory = dirname($path);
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
match($format) {
'json' => file_put_contents($path, json_encode($data, JSON_PRETTY_PRINT)),
'csv' => $this->saveCsvReport($data, $path),
default => file_put_contents($path, serialize($data)),
};
$this->info("Report saved to: {$path}");
}
protected function saveCsvReport(array $data, string $path): void
{
if (empty($data)) return;
$file = fopen($path, 'w');
$headers = array_keys($data[0]);
fputcsv($file, $headers);
foreach ($data as $row) {
fputcsv($file, $row);
}
fclose($file);
}
}৯.৫.৫ Enhanced Service Provider with Command Registration
php
// packages/DevMaster/InvoiceBuilder/src/InvoiceBuilderServiceProvider.php এ commands registration যোগ করুন
use Illuminate\Console\Scheduling\Schedule;
use DevMaster\InvoiceBuilder\Console\Commands\InstallCommand;
use DevMaster\InvoiceBuilder\Console\Commands\PruneInvoicesCommand;
use DevMaster\InvoiceBuilder\Console\Commands\StatusCommand;
use DevMaster\InvoiceBuilder\Console\Commands\GenerateReportCommand;
/**
* Bootstrap any application services.
*/
public function boot()
{
// Publishing assets
if ($this->app->runningInConsole()) {
$this->publishAssets();
$this->publishMigrations();
$this->registerCommands();
$this->registerScheduledTasks();
}
// Load package resources
$this->loadPackageResources();
// Register additional services
$this->registerAdditionalServices();
// Register security features
$this->registerSecurityFeatures();
// Register factory guessing
$this->registerFactories();
}
/**
* Register console commands
*/
protected function registerCommands()
{
if (!config('invoice-builder.commands.enabled', true)) {
return;
}
$this->commands([
InstallCommand::class,
PruneInvoicesCommand::class,
StatusCommand::class,
GenerateReportCommand::class,
]);
}
/**
* Register scheduled tasks
*/
protected function registerScheduledTasks()
{
if (!config('invoice-builder.commands.scheduling.enabled', false)) {
return;
}
$this->app->booted(function () {
$schedule = $this->app->make(Schedule::class);
// Prune old invoices
if (config('invoice-builder.commands.maintenance.prune_enabled', true)) {
$schedule->command('invoice-builder:prune --force')
->cron(config('invoice-builder.commands.scheduling.prune_schedule', 'daily'))
->withoutOverlapping()
->runInBackground();
}
// Check for overdue invoices
$schedule->command('invoice-builder:check-overdue')
->cron(config('invoice-builder.commands.scheduling.overdue_check_schedule', 'hourly'))
->withoutOverlapping();
// Cleanup temp files
$schedule->command('invoice-builder:cleanup')
->cron(config('invoice-builder.commands.scheduling.cleanup_temp_files', 'weekly'))
->withoutOverlapping();
});
}৯.৫.৬ Automation and Notification Features
Overdue Check Command:
php
// packages/DevMaster/InvoiceBuilder/src/Console/Commands/CheckOverdueCommand.php
<?php
namespace DevMaster\InvoiceBuilder\Console\Commands;
use Illuminate\Console\Command;
use DevMaster\InvoiceBuilder\Models\Invoice;
use Illuminate\Support\Facades\Notification;
class CheckOverdueCommand extends Command
{
protected $signature = 'invoice-builder:check-overdue
{--notify : Send notifications for overdue invoices}
{--update-status : Update invoice status to overdue}';
protected $description = 'Check for overdue invoices and optionally send notifications';
public function handle(): int
{
$overdueInvoices = Invoice::whereIn('status', ['sent', 'pending'])
->where('due_date', '<', now())
->get();
if ($overdueInvoices->isEmpty()) {
$this->info('No overdue invoices found.');
return self::SUCCESS;
}
$this->line("Found <comment>{$overdueInvoices->count()}</comment> overdue invoices.");
if ($this->option('update-status')) {
$updated = 0;
foreach ($overdueInvoices as $invoice) {
$invoice->update(['status' => 'overdue']);
$updated++;
}
$this->info("Updated status for {$updated} invoices.");
}
if ($this->option('notify')) {
$this->sendOverdueNotifications($overdueInvoices);
}
return self::SUCCESS;
}
protected function sendOverdueNotifications($overdueInvoices): void
{
$this->line('Sending overdue notifications...');
$progressBar = $this->output->createProgressBar($overdueInvoices->count());
foreach ($overdueInvoices as $invoice) {
// Send notification logic would go here
// Notification::send($invoice->customer, new InvoiceOverdueNotification($invoice));
$progressBar->advance();
}
$progressBar->finish();
$this->line('');
$this->info('Overdue notifications sent.');
}
}৯.৫.৭ Testing Console Commands
Command Testing:
bash
# Installation
php artisan invoice-builder:install --no-migrate --dry-run
# Status check
php artisan invoice-builder:status --detailed --check-health
# Maintenance
php artisan invoice-builder:prune --dry-run --days=30
# Reports
php artisan invoice-builder:report summary --period=month --format=table
php artisan invoice-builder:report detailed --format=csv --output=storage/reports/invoices.csv
# Overdue check
php artisan invoice-builder:check-overdue --notifyCommand Unit Testing:
php
php artisan tinker
>>> // Test command registration
>>> Artisan::all()['invoice-builder:install'] instanceof DevMaster\InvoiceBuilder\Console\Commands\InstallCommand;
>>> // Test command execution
>>> Artisan::call('invoice-builder:status');
>>> Artisan::output();
>>> // Test with options
>>> Artisan::call('invoice-builder:prune', ['--dry-run' => true, '--days' => 30]);Scheduling Verification:
bash
# Check scheduled tasks
php artisan schedule:list | grep invoice
# Test scheduling
php artisan schedule:run --verbose
# Preview scheduled commands
php artisan schedule:list৯.৬ Chapter 9 Checkpoint
এই chapter শেষে আপনার InvoiceBuilder package এ আছে:
- Complete CLI Interface - Installation, maintenance, and reporting commands
- Smart Installation - Automated setup with progress tracking and validation
- Maintenance Automation - Pruning, cleanup, and health checks
- Report Generation - Multiple formats and time periods
- Task Scheduling - Automated background tasks with configuration
- Status Monitoring - Health checks and detailed statistics
- Dry-run Support - Safe testing of destructive operations
- Production Safety - Confirmation prompts and backup features
Test Commands Summary:
bash
# Installation and setup
php artisan invoice-builder:install --no-migrate
# Status and health
php artisan invoice-builder:status --detailed --check-health
# Maintenance operations
php artisan invoice-builder:prune --dry-run --days=30
# Reporting
php artisan invoice-builder:report summary --period=month
# Overdue management
php artisan invoice-builder:check-overdue --notify
# Scheduling
php artisan schedule:list | grep invoiceNext Chapter: Chapter 9 এ আমরা অ্যাডভান্সড প্যাকেজ টেস্টিং এবং কোয়ালিটি অ্যাসিউরেন্স নিয়ে আলোচনা করবো।
Key Concepts Mastered:
- Professional command architecture with progress tracking
- Safe maintenance operations with backup and dry-run
- Automated task scheduling with configuration
- Multi-format report generation
- Health checking and status monitoring
- Production-ready automation with safety measures