First Commit

This commit is contained in:
marco
2026-09-01 10:46:29 -03:00
commit bf96b77e9c
10000 changed files with 3508600 additions and 0 deletions
Executable
+24
View File
@@ -0,0 +1,24 @@
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.php]
indent_size = 4
[*.js]
indent_size = 4
[*.json]
indent_size = 2
[*.twig]
indent_size = 4
[*.md]
trim_trailing_whitespace = false
Executable
+138
View File
@@ -0,0 +1,138 @@
# .env file
# Application settings
APP_NAME=FluxoDeCaixaPro
APP_KEY=base64:UyhxNfcM54vUsw9aNjsMenLjjsK5vTTdIGNLHpBdG4g=
APP_ENV=production
APP_DEBUG=true
APP_URL=http://fluxodecaixapro.syspanel.com.br
# Database configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=evoluterSys5
DB_USERNAME=evoluterSys5
DB_PASSWORD=Tech788959ax10
APP_MULTI_LANG=true
APP_LANG_DEFAULT=pt_BR
# Editor configuration
# Session configuration
SESSION_DRIVER=file # options: file, database, redis, etc.
SESSION_LIFETIME=480 # in minutes
# Mail configuration (example for SMTP)
MAIL_TRANSPORT=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=1c2c80d1d45507
MAIL_PASSWORD=e32909821a29de
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=marcocosta@gmx.com
MAIL_FROM_NAME=MarcoCosta
MAILER_DSN=smtp://1c2c80d1d45507:e32909821a29de@sandbox.smtp.mailtrap.io:2525?encryption=tls
MAIL_URL=syscms.syspanel.com.br
# Logging configuration
LOG_CHANNEL=stack # options: daily, single, syslog, etc.
# JWT Secret for authentication (if you're using JWT)
JWT_SECRET=your_jwt_secret_key
# Other third-party services
PUSHER_APP_ID=your_pusher_app_id
PUSHER_APP_KEY=your_pusher_app_key
PUSHER_APP_SECRET=your_pusher_app_secret
# Redis configuration (if you're using Redis)
REDIS_HOST=syscms-redis
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_DB=0
# Storage paths (example for image uploads)
UPLOADS_DIR=/var/www/sysmvc/public/uploads
# Other sensitive credentials
STRIPE_SECRET_KEY=your_stripe_secret_key
STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
# PayPal
PAYPAL_CLIENT_ID=your_paypal_client_id
PAYPAL_SECRET=your_paypal_secret_key
PAYPAL_MODE=sandbox # ou "live" para produção
# MercadoPago
MERCADOPAGO_ACCESS_TOKEN=your_access_token
MERCADOPAGO_PUBLIC_KEY=your_public_key
# Pagar.me
PAGARME_API_KEY=your_api_key
PAGARME_ENCRYPTION_KEY=your_encryption_key
# Braintree
BRAINTREE_MERCHANT_ID=your_merchant_id
BRAINTREE_PUBLIC_KEY=your_public_key
BRAINTREE_PRIVATE_KEY=your_private_key
# PagSeguro
PAGSEGURO_EMAIL=your_pagseguro_email
PAGSEGURO_TOKEN=your_pagseguro_token
API_PUBLIC_KEY=9d8f87ae56d7806759d59a8e1119b47d
API_SECRET_KEY=7958d0834f85a08beef4d4d23cd1d184f9dcfc2b60b2c30f0a67a677960d62c2
# Optional for development
APP_TIMEZONE=America/Sao_Paulo
IMG_QUALITY = 40
IMG_FORMAT = jpeg
# PHP Configuration
# Limite de memória do PHP (aumente se precisar processar muitos dados)
PHP_MEMORY_LIMIT=1024M
# Tempo máximo de execução de scripts (evita timeout em processos longos)
PHP_MAX_EXECUTION_TIME=300
# Tempo máximo que um script pode aguardar por entrada (upload, por exemplo)
PHP_MAX_INPUT_TIME=120
# Tamanho máximo de arquivos para upload
PHP_UPLOAD_MAX_FILESIZE=256M
# Tamanho máximo de POST (afeta upload de arquivos grandes)
PHP_POST_MAX_SIZE=256M
# Número máximo de arquivos enviados por requisição
PHP_MAX_FILE_UPLOADS=50
# Exibir erros do PHP (use false em produção)
PHP_DISPLAY_ERRORS=false
# Exibir startup errors do PHP
PHP_DISPLAY_STARTUP_ERRORS=true
# Nível de erro do PHP (E_ALL para capturar tudo, E_ERROR para apenas erros graves)
PHP_ERROR_REPORTING=E_ALL
# Habilita ou desabilita logs de erro do PHP
PHP_LOG_ERRORS=true
# Arquivo de log de erros do PHP
PHP_ERROR_LOG=/var/log/php_errors.log
# Arquivo de log de erros gerais
ERROR_REPORTING=true
# Arquivo de log de envio de emails
LOG_EMAILS=true
Executable
+7
View File
@@ -0,0 +1,7 @@
RewriteEngine on
# Allow access to storage directory directly
RewriteRule ^storage - [L]
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]
Executable
+36
View File
@@ -0,0 +1,36 @@
SysCMS Commercial License
Copyright (c) 2025 Marco Costa <contato@syspanel.com.br>
All rights reserved.
This software is licensed, not sold.
Permission is granted to the purchaser to use this software
for internal or commercial purposes, subject to the terms below.
1. You may install and use the software on systems owned or
controlled by you or your organization.
2. You may modify the software for your own internal use.
3. You may NOT redistribute, sublicense, sell, rent, lease,
share, publish, or make available this software, in whole
or in part, to any third party.
4. You may NOT resell this software or include it as part of
another product or service without prior written permission
from the copyright holder.
5. This license does NOT grant any rights to the source code
beyond the right to use and modify it internally.
6. This license is non-transferable unless explicitly authorized
in writing.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DAMAGES
ARISING FROM THE USE OR INABILITY TO USE THIS SOFTWARE.
BIN
View File
Binary file not shown.
Executable
+1769
View File
File diff suppressed because one or more lines are too long
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Illuminate\Database\Capsule\Manager as Capsule;
class ActivityLogsClearCommand extends Command
{
protected static $defaultName = 'logs:activity-clear';
protected static $defaultDescription = 'Limpa todos os registros da tabela activity_logs';
protected function configure(): void
{
$this->setHelp('Este comando executa um TRUNCATE na tabela activity_logs para remover todo o histórico de atividades.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var \Symfony\Component\Console\Helper\QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new ConfirmationQuestion('Tem certeza que deseja limpar TODOS os logs de atividade? (y/n) [n]: ', false);
if (!$helper->ask($input, $output, $question)) {
$output->writeln('<info>Operação cancelada.</info>');
return Command::SUCCESS;
}
try {
Capsule::table('activity_logs')->truncate();
$output->writeln('<info>Tabela activity_logs limpa com sucesso!</info>');
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln('<error>Erro ao limpar logs: ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Kernel\Application;
class AppAboutCommand extends Command
{
protected static $defaultName = 'app:about';
protected static $defaultDescription = 'Exibe informações detalhadas sobre a aplicação';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Sobre a Aplicação SysCMS</info>');
$output->writeln('===========================');
$table = new Table($output);
$table->setHeaders(['Informação', 'Valor']);
$table->addRow(['Nome', $_ENV['APP_NAME'] ?? 'SysCMS']);
$table->addRow(['Versão', '1.0.0']);
$table->addRow(['Ambiente', $_ENV['APP_ENV'] ?? 'local']);
$table->addRow(['Debug', ($_ENV['APP_DEBUG'] ?? 'false') === 'true' ? '<info>Ativo</info>' : '<comment>Inativo</comment>']);
$table->addRow(['URL', $_ENV['APP_URL'] ?? 'http://localhost']);
$table->addRow(['PHP Version', PHP_VERSION]);
$table->addRow(['OS', PHP_OS]);
$table->addRow(['Database', $_ENV['DB_DATABASE'] ?? 'syscms']);
$table->addRow(['Database Driver', $_ENV['DB_CONNECTION'] ?? 'mysql']);
$table->addRow(['Timezone', date_default_timezone_get()]);
$table->addRow(['Locale', $_SESSION['lang'] ?? 'pt_BR']);
$table->render();
return Command::SUCCESS;
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AppInfoCommand extends Command
{
protected static $defaultName = 'app:info';
protected static $defaultDescription = 'Displays application information';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln([
'<info>SysCMS - PHP Content Management System</info>',
'========================================',
'',
]);
$output->writeln("PHP Version: " . PHP_VERSION);
$output->writeln("Environment: " . ($_ENV['APP_ENV'] ?? 'production'));
$output->writeln("Debug Mode: " . (($_ENV['APP_DEBUG'] ?? 'false') === 'true' ? 'Enabled' : 'Disabled'));
$output->writeln("URL: " . ($_ENV['APP_URL'] ?? 'Not set'));
$output->writeln([
'',
'<comment>Database:</comment>',
"Connection: " . ($_ENV['DB_CONNECTION'] ?? 'mysql'),
"Host: " . ($_ENV['DB_HOST'] ?? 'localhost'),
"Database: " . ($_ENV['DB_DATABASE'] ?? 'syscms'),
]);
return Command::SUCCESS;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\ArrayInput;
class AppOptimizeCommand extends Command
{
protected static $defaultName = 'app:optimize';
protected static $defaultDescription = 'Otimiza a aplicação limpando caches e bancos de dados (comandos combinados)';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Iniciando otimização completa do sistema...</info>');
// 1. Clear Cache
$output->writeln("\n<comment>Limpando caches...</comment>");
$cacheCommand = $this->getApplication()->find('cache:clear');
$cacheCommand->run(new ArrayInput([]), $output);
// 2. Clear Logs
$output->writeln("\n<comment>Limpando logs...</comment>");
$logsCommand = $this->getApplication()->find('logs:clear');
$logsCommand->run(new ArrayInput([]), $output);
// 3. Optimize DB
$output->writeln("\n<comment>Otimizando banco de dados...</comment>");
$dbCommand = $this->getApplication()->find('db:optimize');
$dbCommand->run(new ArrayInput([]), $output);
$output->writeln("\n<info>Aplicação otimizada com sucesso!</info>");
return Command::SUCCESS;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AppRenameCommand extends Command
{
protected static $defaultName = 'app:rename';
protected static $defaultDescription = 'Altera o nome da aplicação no arquivo .env';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O novo nome da aplicação');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$newName = $input->getArgument('name');
$envFile = dirname(__DIR__, 3) . '/.env';
if (!file_exists($envFile)) {
$output->writeln('<error>Arquivo .env não encontrado.</error>');
return Command::FAILURE;
}
$content = file_get_contents($envFile);
$pattern = "/APP_NAME=(.*)/";
$replacement = "APP_NAME=\"{$newName}\"";
if (preg_match($pattern, $content)) {
$newContent = preg_replace($pattern, $replacement, $content);
} else {
$newContent = $content . "\nAPP_NAME=\"{$newName}\"";
}
if (file_put_contents($envFile, $newContent)) {
$output->writeln("<info>Aplicação renomeada para '{$newName}' com sucesso!</info>");
return Command::SUCCESS;
}
$output->writeln('<error>Falha ao renomear a aplicação.</error>');
return Command::FAILURE;
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CacheClearCommand extends Command
{
protected static $defaultName = 'cache:clear';
protected static $defaultDescription = 'Clears the application cache';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Clearing cache...');
$cacheDirs = [
dirname(__DIR__, 3) . '/cache/twig',
// Add other cache directories here
];
$hasError = false;
foreach ($cacheDirs as $dir) {
if (is_dir($dir)) {
$output->write("Limpando <info>{$dir}</info>... ");
if ($this->deleteDirectory($dir)) {
$output->writeln('<info>OK</info>');
// Recreate empty directory
if (!is_dir($dir)) {
mkdir($dir, 0777, true);
chmod($dir, 0777);
}
} else {
$output->writeln('<error>FALHA</error>');
$hasError = true;
}
}
}
if ($hasError) {
$output->writeln('<error>Alguns caches não puderam ser limpos.</error>');
return Command::FAILURE;
}
$output->writeln('<info>Cache limpo com sucesso!</info>');
return Command::SUCCESS;
}
private function deleteDirectory($dir) {
if (!file_exists($dir)) {
return true;
}
if (!is_dir($dir)) {
return unlink($dir);
}
foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') {
continue;
}
if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
return false;
}
}
return rmdir($dir);
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Modules\Auth\Models\User;
use Carbon\Carbon;
class CreateSuperAdminCommand extends Command
{
protected static $defaultName = 'user:create-super-admin';
protected static $defaultDescription = 'Creates the default Super Admin user';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$email = 'superadmin@syscms.com';
$password = 'SuperAdmin123!';
try {
// Remove global scopes to ensure we can find the user regardless of company context
$user = User::withoutGlobalScopes()->where('users_email', $email)->first();
if ($user) {
$user->users_role = 'super_admin';
$user->users_password = password_hash($password, PASSWORD_DEFAULT);
$user->users_status = 'active';
$user->save();
$output->writeln('<info>Usuário Super Admin atualizado com sucesso!</info>');
} else {
$user = new User();
$user->users_name = 'Super Admin';
$user->users_email = $email;
$user->users_password = password_hash($password, PASSWORD_DEFAULT);
$user->users_role = 'super_admin';
$user->users_status = 'active';
$user->save();
$output->writeln('<info>Usuário Super Admin criado com sucesso!</info>');
}
$output->writeln([
'',
'Credenciais:',
"Email: $email",
"Senha: $password",
"Role: super_admin",
''
]);
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Modules\Auth\Models\User;
class CreateUserCommand extends Command
{
protected static $defaultName = 'user:create';
protected static $defaultDescription = 'Creates a new user';
protected function configure(): void
{
$this
->addArgument('email', InputArgument::OPTIONAL, 'User email');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
// Email
$email = $input->getArgument('email');
if (!$email) {
$question = new Question('Email do usuário: ');
$email = $helper->ask($input, $output, $question);
}
if (User::where('users_email', $email)->exists()) {
$output->writeln("<error>Já existe um usuário com o email {$email}</error>");
return Command::FAILURE;
}
// Name
$question = new Question('Nome do usuário: ');
$name = $helper->ask($input, $output, $question);
// Password
$question = new Question('Senha: ');
$question->setHidden(true);
$question->setHiddenFallback(false);
$password = $helper->ask($input, $output, $question);
// Role
$question = new Question('Role (padrão: user): ', 'user');
$role = $helper->ask($input, $output, $question);
// Bus ID
$question = new Question('ID Bus (opcional): ');
$busId = $helper->ask($input, $output, $question);
try {
$user = new User();
$user->users_name = $name;
$user->users_email = $email;
$user->users_password = password_hash($password, PASSWORD_DEFAULT);
$user->users_role = $role;
$user->users_bus_id = $busId;
$user->users_status = 'active';
$user->save();
$output->writeln('<info>Usuário criado com sucesso!</info>');
$output->writeln("ID: {$user->users_id}");
$output->writeln("Nome: {$user->users_name}");
$output->writeln("Email: {$user->users_email}");
$output->writeln("Role: {$user->users_role}");
$output->writeln("ID Bus: " . ($user->users_bus_id ?? '-'));
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao criar usuário: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+73
View File
@@ -0,0 +1,73 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class DbBackupCommand extends Command
{
protected static $defaultName = 'db:backup';
protected static $defaultDescription = 'Cria um backup do banco de dados';
protected function configure(): void
{
$this
->setHelp('Este comando gera um dump SQL do banco de dados atual.')
->addOption('path', 'p', InputOption::VALUE_OPTIONAL, 'Diretório para salvar o backup', 'storage/backups')
->addOption('compress', 'c', InputOption::VALUE_NONE, 'Comprimir o backup (gzip)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Iniciando backup do banco de dados...</info>');
$host = $_ENV['DB_HOST'] ?? 'localhost';
$port = $_ENV['DB_PORT'] ?? '3306';
$database = $_ENV['DB_DATABASE'] ?? 'syscms';
$username = $_ENV['DB_USERNAME'] ?? 'root';
$password = $_ENV['DB_PASSWORD'] ?? '';
$backupDir = $input->getOption('path');
// Se o caminho for relativo, resolvemos a partir da raiz do projeto
if (!str_starts_with($backupDir, '/')) {
$backupDir = dirname(__DIR__, 3) . '/' . ltrim($backupDir, '/');
}
if (!is_dir($backupDir)) {
if (!mkdir($backupDir, 0755, true)) {
$output->writeln("<error>Não foi possível criar o diretório de backup: {$backupDir}</error>");
return Command::FAILURE;
}
}
$filename = $database . '_' . date('Y-m-d_H-i-s') . '.sql';
$fullPath = rtrim($backupDir, '/') . '/' . $filename;
// Escapamos a senha para o shell
$escapedPassword = escapeshellarg($password);
// Usamos mysqldump para criar o backup
// Note: Se o mysqldump não estiver no PATH, este comando pode falhar
$command = "mysqldump --host={$host} --port={$port} --user={$username} --password={$escapedPassword} {$database} > " . escapeshellarg($fullPath);
if ($input->getOption('compress')) {
$command .= " && gzip " . escapeshellarg($fullPath);
$fullPath .= '.gz';
}
exec($command . " 2>&1", $outputArray, $resultCode);
if ($resultCode === 0) {
$output->writeln("<info>Backup concluído com sucesso: {$fullPath}</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Erro ao realizar o backup (Código: {$resultCode}).</error>");
$output->writeln("<error>Saída: " . implode("\n", $outputArray) . "</error>");
return Command::FAILURE;
}
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\Capsule\Manager as Capsule;
class DbExportCommand extends Command
{
protected static $defaultName = 'db:export';
protected static $defaultDescription = 'Exporta os dados de uma tabela para CSV ou JSON';
protected function configure(): void
{
$this
->addArgument('table', InputArgument::REQUIRED, 'O nome da tabela')
->addOption('format', 'f', InputOption::VALUE_OPTIONAL, 'O formato do arquivo (csv, json)', 'csv')
->addOption('path', 'p', InputOption::VALUE_OPTIONAL, 'O caminho para salvar o arquivo', 'storage/exports');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$tableName = $input->getArgument('table');
$format = strtolower($input->getOption('format'));
$path = $input->getOption('path');
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
if (!Capsule::schema()->hasTable($tableName)) {
$output->writeln("<error>Tabela '{$tableName}' não encontrada no banco de dados.</error>");
return Command::FAILURE;
}
$results = $connection->table($tableName)->get();
if ($results->isEmpty()) {
$output->writeln("<info>A tabela '{$tableName}' está vazia.</info>");
return Command::SUCCESS;
}
$output->writeln("<info>Exportando dados da tabela: {$tableName}</info>");
$directory = dirname(__DIR__, 3) . '/' . ltrim($path, '/');
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$filename = "{$tableName}_" . date('Y-m-d_H-i-s') . ".{$format}";
$fullPath = rtrim($directory, '/') . '/' . $filename;
if ($format === 'json') {
file_put_contents($fullPath, json_encode($results, JSON_PRETTY_PRINT));
} elseif ($format === 'csv') {
$fp = fopen($fullPath, 'w');
// Adiciona o cabeçalho
$headers = array_keys((array) $results[0]);
fputcsv($fp, $headers);
foreach ($results as $row) {
fputcsv($fp, (array) $row);
}
fclose($fp);
} else {
$output->writeln("<error>Formato '{$format}' não suportado.</error>");
return Command::FAILURE;
}
$output->writeln("<info>Dados exportados com sucesso para: {$fullPath}</info>");
return Command::SUCCESS;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
class DbImportCommand extends Command
{
protected static $defaultName = 'db:import';
protected static $defaultDescription = 'Importa um arquivo SQL para o banco de dados';
protected function configure(): void
{
$this
->addArgument('file', InputArgument::REQUIRED, 'O caminho do arquivo SQL a ser importado');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$file = $input->getArgument('file');
if (!file_exists($file)) {
$output->writeln("<error>Arquivo não encontrado: {$file}</error>");
return Command::FAILURE;
}
$output->writeln("<info>Importando arquivo: {$file}</info>");
try {
$sql = file_get_contents($file);
if (!$sql) {
$output->writeln("<error>Falha ao ler o arquivo SQL ou arquivo vazio.</error>");
return Command::FAILURE;
}
$connection = Capsule::connection();
$queries = $this->splitSqlQueries($sql);
$totalQueries = count($queries);
$successCount = 0;
$output->writeln("<comment>Total de queries encontradas: {$totalQueries}</comment>");
foreach ($queries as $index => $query) {
$query = trim($query);
if (empty($query)) {
continue;
}
try {
$connection->statement($query);
$successCount++;
$output->writeln("<info>Query " . ($index + 1) . "/{$totalQueries} executada com sucesso</info>");
} catch (\Exception $e) {
$output->writeln("<error>Erro na query " . ($index + 1) . ": " . $e->getMessage() . "</error>");
$output->writeln("<comment>Query: " . substr($query, 0, 100) . "...</comment>");
}
}
$output->writeln("<info>Importação concluída. {$successCount}/{$totalQueries} queries executadas com sucesso.</info>");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao importar arquivo: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
private function splitSqlQueries(string $sql): array
{
$queries = [];
$current = '';
$inString = false;
$stringChar = '';
$length = strlen($sql);
for ($i = 0; $i < $length; $i++) {
$char = $sql[$i];
if (($char === "'" || $char === '"') && ($i === 0 || $sql[$i - 1] !== '\\')) {
if (!$inString) {
$inString = true;
$stringChar = $char;
} elseif ($char === $stringChar) {
$inString = false;
}
}
if (!$inString && $char === ';') {
$queries[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
$current = trim($current);
if (!empty($current)) {
$queries[] = $current;
}
return array_filter($queries, function($query) {
return !empty(trim($query));
});
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbListCommand extends Command
{
protected static $defaultName = 'db:list';
protected static $defaultDescription = 'Lista todas as tabelas do banco de dados';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
$output->writeln("<info>Tabelas no banco de dados: {$dbName}</info>");
$tables = $connection->select('SHOW TABLES');
$key = "Tables_in_{$dbName}";
if (empty($tables)) {
$output->writeln('<info>Nenhuma tabela encontrada no banco de dados.</info>');
return Command::SUCCESS;
}
$table = new Table($output);
$table->setHeaders(['Tabela', 'Total de Registros']);
foreach ($tables as $t) {
$tableName = $t->$key;
$count = $connection->table($tableName)->count();
$table->addRow([$tableName, $count]);
}
$table->render();
$output->writeln("<info>Total de tabelas: " . count($tables) . "</info>");
return Command::SUCCESS;
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbMonitorCommand extends Command
{
protected static $defaultName = 'db:monitor';
protected static $defaultDescription = 'Monitora o status atual do banco de dados e processos ativos';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
$output->writeln("<info>Monitorando banco de dados: {$dbName}</info>");
$output->writeln("===============================================");
// Active processes
$processes = $connection->select('SHOW PROCESSLIST');
$output->writeln("<comment>Processos Ativos:</comment>");
$processTable = new Table($output);
$processTable->setHeaders(['ID', 'User', 'Host', 'Command', 'Time', 'State', 'Info']);
foreach ($processes as $p) {
$processTable->addRow([
$p->Id,
$p->User,
$p->Host,
$p->Command,
$p->Time,
$p->State,
substr($p->Info ?? '', 0, 50) . (strlen($p->Info ?? '') > 50 ? '...' : '')
]);
}
$processTable->render();
$output->writeln("");
// Database status variables
$status = $connection->select("SHOW STATUS WHERE Variable_name IN ('Threads_connected', 'Threads_running', 'Uptime', 'Questions')");
$output->writeln("<comment>Variáveis de Status:</comment>");
$statusTable = new Table($output);
$statusTable->setHeaders(['Variável', 'Valor']);
foreach ($status as $s) {
$statusTable->addRow([$s->Variable_name, $s->Value]);
}
$statusTable->render();
return Command::SUCCESS;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
class DbOptimizeCommand extends Command
{
protected static $defaultName = 'db:optimize';
protected static $defaultDescription = 'Otimiza as tabelas do banco de dados';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
$output->writeln("<info>Otimizando banco de dados: {$dbName}</info>");
$tables = $connection->select('SHOW TABLES');
$key = "Tables_in_{$dbName}";
foreach ($tables as $table) {
$tableName = $table->$key;
$output->write("Otimizando tabela: <comment>{$tableName}</comment>... ");
try {
$connection->statement("OPTIMIZE TABLE {$tableName}");
$output->writeln('<info>OK</info>');
} catch (\Exception $e) {
$output->writeln('<error>Erro ao otimizar</error>');
}
}
$output->writeln('<info>Otimização concluída!</info>');
return Command::SUCCESS;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbQueryCommand extends Command
{
protected static $defaultName = 'db:query';
protected static $defaultDescription = 'Executa uma query SQL diretamente no banco de dados';
protected function configure(): void
{
$this
->addArgument('query', InputArgument::REQUIRED, 'A query SQL a ser executada');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$query = $input->getArgument('query');
$connection = Capsule::connection();
$output->writeln("<info>Executando query:</info> <comment>{$query}</comment>");
try {
if (stripos($query, 'select') === 0 || stripos($query, 'show') === 0 || stripos($query, 'describe') === 0) {
$results = $connection->select($query);
if (empty($results)) {
$output->writeln('<info>Nenhum resultado encontrado.</info>');
return Command::SUCCESS;
}
$table = new Table($output);
$headers = array_keys((array) $results[0]);
$table->setHeaders($headers);
foreach ($results as $row) {
$table->addRow((array) $row);
}
$table->render();
$output->writeln("<info>Total de registros: " . count($results) . "</info>");
} else {
$affected = $connection->affectingStatement($query);
$output->writeln("<info>Query executada com sucesso. Linhas afetadas: {$affected}</info>");
}
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao executar query: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbRelationshipsCommand extends Command
{
protected static $defaultName = 'db:relationships';
protected static $defaultDescription = 'Exibe as chaves estrangeiras de uma tabela';
protected function configure(): void
{
$this
->addArgument('table', InputArgument::REQUIRED, 'O nome da tabela');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$tableName = $input->getArgument('table');
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
$output->writeln("<info>Chaves Estrangeiras da Tabela: {$tableName}</info>");
try {
$query = "
SELECT
COLUMN_NAME,
CONSTRAINT_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM
INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE
TABLE_SCHEMA = ? AND
TABLE_NAME = ? AND
REFERENCED_TABLE_NAME IS NOT NULL";
$relationships = $connection->select($query, [$dbName, $tableName]);
if (empty($relationships)) {
$output->writeln('<comment>Nenhuma chave estrangeira encontrada para esta tabela.</comment>');
return Command::SUCCESS;
}
$table = new Table($output);
$table->setHeaders(['Coluna', 'Constraint', 'Tabela Referenciada', 'Coluna Referenciada']);
foreach ($relationships as $rel) {
$table->addRow([
$rel->COLUMN_NAME,
$rel->CONSTRAINT_NAME,
$rel->REFERENCED_TABLE_NAME,
$rel->REFERENCED_COLUMN_NAME
]);
}
$table->render();
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao buscar relacionamentos: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DbSeedCommand extends Command
{
protected static $defaultName = 'db:seed';
protected static $defaultDescription = 'Executa as classes de seeder';
protected function configure(): void
{
$this
->addArgument('class', InputArgument::OPTIONAL, 'O nome da classe seeder específica para rodar');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$className = $input->getArgument('class');
$seedersPath = dirname(__DIR__, 3) . '/database/seeders';
if (!is_dir($seedersPath)) {
$output->writeln('<error>Diretório de seeders não encontrado.</error>');
return Command::FAILURE;
}
if ($className) {
return $this->runSeeder($className, $seedersPath, $output);
}
// Se não especificou classe, tenta rodar o DatabaseSeeder ou todos os arquivos .php
$dbSeederFile = $seedersPath . '/DatabaseSeeder.php';
if (file_exists($dbSeederFile)) {
return $this->runSeeder('DatabaseSeeder', $seedersPath, $output);
}
$files = glob($seedersPath . '/*.php');
if (empty($files)) {
$output->writeln('<info>Nenhum seeder encontrado.</info>');
return Command::SUCCESS;
}
foreach ($files as $file) {
$name = basename($file, '.php');
if ($name === 'DatabaseSeeder') continue; // Já rodamos se existisse
$this->runSeeder($name, $seedersPath, $output);
}
return Command::SUCCESS;
}
protected function runSeeder(string $name, string $path, OutputInterface $output): int
{
$file = $path . '/' . $name . '.php';
if (!file_exists($file)) {
$output->writeln("<error>Seeder não encontrado: {$name}</error>");
return Command::FAILURE;
}
$output->write("Rodando seeder: <info>{$name}</info>... ");
try {
require_once $file;
if (class_exists($name)) {
$seeder = new $name();
if (method_exists($seeder, 'run')) {
$seeder->run();
$output->writeln('<info>OK</info>');
return Command::SUCCESS;
} else {
$output->writeln('<comment>Método run() não encontrado.</comment>');
return Command::FAILURE;
}
} else {
$output->writeln("<error>Classe {$name} não encontrada no arquivo.</error>");
return Command::FAILURE;
}
} catch (\Exception $e) {
$output->writeln("<error>FALHA</error>");
$output->writeln("<error>Erro: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbShowCommand extends Command
{
protected static $defaultName = 'db:show';
protected static $defaultDescription = 'Exibe informações sobre o banco de dados';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$connection = Capsule::connection();
$dbName = $connection->getDatabaseName();
$output->writeln("<info>Banco de Dados: {$dbName}</info>");
$output->writeln("Driver: " . $connection->getDriverName());
$tables = $connection->select('SHOW TABLE STATUS');
$table = new Table($output);
$table->setHeaders(['Tabela', 'Engine', 'Rows', 'Size (MB)', 'Collation']);
foreach ($tables as $t) {
$size = round(($t->Data_length + $t->Index_length) / 1024 / 1024, 2);
$table->addRow([
$t->Name,
$t->Engine,
$t->Rows,
$size,
$t->Collation
]);
}
$table->render();
return Command::SUCCESS;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class DbSyncSisTablesCommand extends Command
{
protected static $defaultName = 'db:sync-sis-tables';
protected static $defaultDescription = 'Substitui as tabelas sis_*** no evoluterSys2.sql pelas tabelas de syscms.sql';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$targetFile = 'evoluterSys2.sql';
$sourceFile = 'syscms.sql';
if (!file_exists($targetFile)) {
$output->writeln("<error>Arquivo de destino não encontrado: $targetFile</error>");
return Command::FAILURE;
}
if (!file_exists($sourceFile)) {
$output->writeln("<error>Arquivo de origem não encontrado: $sourceFile</error>");
return Command::FAILURE;
}
$output->writeln("<info>Iniciando a extração das tabelas sis_*** de $sourceFile...</info>");
$sisTablesDefinitions = $this->extractSisTables($sourceFile);
if (empty($sisTablesDefinitions)) {
$output->writeln("<error>Nenhuma tabela sis_*** encontrada em $sourceFile.</error>");
return Command::FAILURE;
}
$output->writeln("<info>Encontradas " . count($sisTablesDefinitions) . " tabelas sis_***. Substituindo em $targetFile...</info>");
$this->replaceSisTables($targetFile, $sisTablesDefinitions, $output);
$output->writeln("<info>Operação concluída com sucesso!</info>");
return Command::SUCCESS;
}
private function extractSisTables(string $filePath): array
{
$handle = fopen($filePath, 'r');
if (!$handle) return [];
$tables = [];
$currentTable = null;
$capture = false;
$buffer = "";
while (($line = fgets($handle)) !== false) {
// Detecta início de um CREATE TABLE sis_***
if (preg_match("/CREATE TABLE `?(sis_[^`\s(]+)`?/i", $line, $matches)) {
$currentTable = $matches[1];
$capture = true;
$buffer = $line;
continue;
}
if ($capture) {
$buffer .= $line;
// Detecta fim do CREATE TABLE (assume-se que termina com ); ou ); seguido de comentários/inserts se for dump compactado,
// mas para simplificar, buscamos o padrão );
if (preg_match("/\);/", $line)) {
$capture = false;
$tables[$currentTable] = $buffer;
$buffer = "";
$currentTable = null;
}
}
}
fclose($handle);
return $tables;
}
private function replaceSisTables(string $filePath, array $definitions, OutputInterface $output): void
{
$tempFile = $filePath . '.tmp';
$inHandle = fopen($filePath, 'r');
$outHandle = fopen($tempFile, 'w');
$skip = false;
$replacedCount = 0;
while (($line = fgets($inHandle)) !== false) {
// Verifica se a linha inicia uma tabela que queremos substituir
if (preg_match("/CREATE TABLE `?(sis_[^`\s(]+)`?/i", $line, $matches)) {
$tableName = $matches[1];
if (isset($definitions[$tableName])) {
$output->writeln("Substituindo tabela: <comment>$tableName</comment>");
fwrite($outHandle, $definitions[$tableName]);
$skip = true;
$replacedCount++;
unset($definitions[$tableName]); // Marca como usada
continue;
}
}
// Se estiver pulando, ignora as linhas até o fim da definição da tabela
if ($skip) {
if (preg_match("/\);/", $line)) {
$skip = false;
}
continue;
}
fwrite($outHandle, $line);
}
// Se sobraram tabelas no source que não existiam no target, podemos adicioná-las ao final ou apenas informar
if (!empty($definitions)) {
$output->writeln("<info>Adicionando novas tabelas sis_*** que não existiam no destino...</info>");
foreach ($definitions as $tableName => $def) {
$output->writeln("Adicionando: <comment>$tableName</comment>");
fwrite($outHandle, "\n" . $def . "\n");
$replacedCount++;
}
}
fclose($inHandle);
fclose($outHandle);
rename($tempFile, $filePath);
$output->writeln("<info>Total de tabelas processadas: $replacedCount</info>");
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
use Symfony\Component\Console\Helper\Table;
class DbTableCommand extends Command
{
protected static $defaultName = 'db:table';
protected static $defaultDescription = 'Exibe a estrutura de uma tabela específica';
protected function configure(): void
{
$this
->addArgument('table', InputArgument::REQUIRED, 'O nome da tabela');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$tableName = $input->getArgument('table');
$connection = Capsule::connection();
try {
$columns = $connection->select("SHOW COLUMNS FROM {$tableName}");
$output->writeln("<info>Estrutura da tabela: {$tableName}</info>");
$table = new Table($output);
$table->setHeaders(['Field', 'Type', 'Null', 'Key', 'Default', 'Extra']);
foreach ($columns as $column) {
$table->addRow([
$column->Field,
$column->Type,
$column->Null,
$column->Key,
$column->Default,
$column->Extra
]);
}
$table->render();
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Tabela '{$tableName}' não encontrada ou erro na query.</error>");
return Command::FAILURE;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\Capsule\Manager as Capsule;
class DbWipeCommand extends Command
{
protected static $defaultName = 'db:wipe';
protected static $defaultDescription = 'Drop all tables, views, and types';
protected function configure(): void
{
$this
->setHelp('This command drops all tables from the database without removing the database itself.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Force the operation to run');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!$input->getOption('force')) {
$output->writeln('<error>Atenção: Este comando irá apagar TODAS as tabelas do banco de dados!</error>');
$output->writeln('<comment>Use a opção --force para confirmar.</comment>');
return Command::FAILURE;
}
$output->writeln('<info>Limpando banco de dados...</info>');
$schema = Capsule::schema();
$tables = Capsule::connection()->select('SHOW FULL TABLES WHERE Table_type = "BASE TABLE"');
$dbName = Capsule::connection()->getDatabaseName();
$key = "Tables_in_{$dbName}";
Capsule::connection()->statement('SET FOREIGN_KEY_CHECKS = 0');
foreach ($tables as $table) {
$tableName = $table->$key;
$output->write("Dropando tabela: <comment>{$tableName}</comment>... ");
$schema->drop($tableName);
$output->writeln('<info>OK</info>');
}
Capsule::connection()->statement('SET FOREIGN_KEY_CHECKS = 1');
$output->writeln('<info>Banco de dados limpo com sucesso!</info>');
return Command::SUCCESS;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Kernel\Application;
class DebugContainerCommand extends Command
{
protected static $defaultName = 'debug:container';
protected static $defaultDescription = 'Lista todos os serviços registrados no container de injeção de dependência';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$app = Application::getInstance();
if (!$app) {
$output->writeln('<error>Não foi possível obter a instância da aplicação.</error>');
return Command::FAILURE;
}
$container = $app->getContainer();
// PHP-DI doesn't easily expose all registered definitions unless we use internal reflection
// or a specific configuration. For simplicity in this environment, we'll list what's common.
$output->writeln('<info>Serviços no Container DI:</info>');
$table = new Table($output);
$table->setHeaders(['ID', 'Status']);
$commonServices = [
\DI\Container::class,
\Kernel\Application::class,
\Kernel\ModuleLoader::class,
\Kernel\View::class,
'db',
'logging',
'mailer'
];
foreach ($commonServices as $service) {
$status = $container->has($service) ? '<info>Registrado</info>' : '<comment>Não definido</comment>';
$table->addRow([$service, $status]);
}
$table->render();
return Command::SUCCESS;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Kernel\Application;
class DebugRouterCommand extends Command
{
protected static $defaultName = 'debug:router';
protected static $defaultDescription = 'Lista detalhes das rotas registradas na aplicação';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$app = Application::getInstance();
if (!$app) {
$output->writeln('<error>Não foi possível obter a instância da aplicação.</error>');
return Command::FAILURE;
}
$routes = $app->getRoutes();
$output->writeln('<info>Rotas Registradas:</info>');
$table = new Table($output);
$table->setHeaders(['Método', 'Caminho', 'Handler']);
foreach ($routes as $route) {
[$method, $path, $handler] = $route;
$handlerStr = is_array($handler) ? implode('@', $handler) : 'Closure';
$table->addRow([$method, $path, $handlerStr]);
}
$table->render();
return Command::SUCCESS;
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class DownCommand extends Command
{
protected static $defaultName = 'down';
protected static $defaultDescription = 'Coloca a aplicação em modo de manutenção';
protected function configure(): void
{
$this
->addOption('message', 'm', InputOption::VALUE_OPTIONAL, 'A mensagem a ser exibida durante a manutenção', 'O sistema está em manutenção.')
->addOption('retry', 'r', InputOption::VALUE_OPTIONAL, 'O tempo estimado para o retorno');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$directory = dirname(__DIR__, 3) . '/storage/framework';
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$downFile = $directory . '/down';
if (file_exists($downFile)) {
$output->writeln('<comment>A aplicação já está em modo de manutenção.</comment>');
return Command::SUCCESS;
}
$payload = [
'time' => time(),
'message' => $input->getOption('message'),
'retry' => $input->getOption('retry'),
];
if (file_put_contents($downFile, json_encode($payload, JSON_PRETTY_PRINT))) {
$output->writeln('<info>Aplicação agora está em modo de manutenção.</info>');
return Command::SUCCESS;
}
$output->writeln('<error>Falha ao colocar a aplicação em modo de manutenção.</error>');
return Command::FAILURE;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
class EnvListCommand extends Command
{
protected static $defaultName = 'env:list';
protected static $defaultDescription = 'Lista as variáveis de ambiente carregadas do .env';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Variáveis de Ambiente (.env):</info>');
$envFile = dirname(__DIR__, 3) . '/.env';
if (!file_exists($envFile)) {
$output->writeln('<error>Arquivo .env não encontrado.</error>');
return Command::FAILURE;
}
$table = new Table($output);
$table->setHeaders(['Variável', 'Valor']);
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Ignorar comentários
if (str_starts_with(trim($line), '#')) {
continue;
}
if (str_contains($line, '=')) {
[$key, $value] = explode('=', $line, 2);
// Ocultar senhas e chaves sensíveis por segurança
if (preg_match('/(PASSWORD|SECRET|KEY|TOKEN)/i', $key)) {
$value = '********';
}
$table->addRow([trim($key), trim($value, '"\' ')]);
}
}
$table->render();
return Command::SUCCESS;
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class KeyGenerateCommand extends Command
{
protected static $defaultName = 'key:generate';
protected static $defaultDescription = 'Gera uma nova chave de aplicação (APP_KEY)';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$key = bin2hex(random_bytes(32));
$envFile = dirname(__DIR__, 3) . '/.env';
if (!file_exists($envFile)) {
$output->writeln("<error>Arquivo .env não encontrado.</error>");
return Command::FAILURE;
}
$content = file_get_contents($envFile);
if (strpos($content, 'APP_KEY=') !== false) {
$content = preg_replace('/APP_KEY=.*/', 'APP_KEY=' . $key, $content);
} else {
$content .= "\nAPP_KEY=" . $key;
}
if (file_put_contents($envFile, $content)) {
$output->writeln("<info>APP_KEY gerada com sucesso!</info>");
$output->writeln("<comment>Chave:</comment> {$key}");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao escrever no arquivo .env.</error>");
return Command::FAILURE;
}
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class LogsClearCommand extends Command
{
protected static $defaultName = 'logs:clear';
protected static $defaultDescription = 'Limpa os arquivos de log do sistema';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$logDir = dirname(__DIR__, 3) . '/logs';
if (!is_dir($logDir)) {
$output->writeln('<error>Diretório de logs não encontrado.</error>');
return Command::FAILURE;
}
$files = glob($logDir . '/*.log');
if (empty($files)) {
$output->writeln('<info>Nenhum arquivo de log para limpar.</info>');
return Command::SUCCESS;
}
foreach ($files as $file) {
$fileName = basename($file);
if (unlink($file)) {
$output->writeln("<info>Arquivo de log '{$fileName}' removido.</info>");
} else {
$output->writeln("<error>Falha ao remover o arquivo '{$fileName}'.</error>");
}
}
return Command::SUCCESS;
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeCommandCommand extends Command
{
protected static $defaultName = 'make:command';
protected static $defaultDescription = 'Cria um novo comando para o syscli';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe do comando (ex: TestCommand)')
->addArgument('command', InputArgument::REQUIRED, 'O comando a ser digitado (ex: app:test)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
$commandName = $input->getArgument('command');
// Garantir que termina com Command
if (!str_ends_with($name, 'Command')) {
$name .= 'Command';
}
$commandsPath = __DIR__;
$filePath = $commandsPath . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O comando '{$name}' já existe.</error>");
return Command::FAILURE;
}
$template = <<<PHP
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class {$name} extends Command
{
protected static \$defaultName = '{$commandName}';
protected static \$defaultDescription = 'Descrição do comando {$commandName}';
protected function configure(): void
{
// Configure argumentos e opções aqui
}
protected function execute(InputInterface \$input, OutputInterface \$output): int
{
// Sua lógica aqui
\$output->writeln('<info>Executando comando {$commandName}...</info>');
return Command::SUCCESS;
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Comando '{$name}' criado com sucesso em app/Console/Commands/</info>");
$output->writeln("<comment>Lembre-se de registrar o comando no syscli se necessário.</comment>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do comando.</error>");
return Command::FAILURE;
}
}
}
+91
View File
@@ -0,0 +1,91 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeControllerCommand extends Command
{
protected static $defaultName = 'make:controller';
protected static $defaultDescription = 'Cria um novo controlador em um módulo específico';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo (ex: blog)')
->addArgument('name', InputArgument::REQUIRED, 'O nome do controlador (ex: PostController)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$module = strtolower($input->getArgument('module'));
$name = $input->getArgument('name');
// Garantir que termina com Controller
if (!str_ends_with($name, 'Controller')) {
$name .= 'Controller';
}
$modulePath = dirname(__DIR__, 3) . '/modules/' . $module;
if (!is_dir($modulePath)) {
$output->writeln("<error>O módulo '{$module}' não existe.</error>");
return Command::FAILURE;
}
$controllersPath = $modulePath . '/classes/Controllers';
if (!is_dir($controllersPath)) {
mkdir($controllersPath, 0755, true);
}
$filePath = $controllersPath . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O controlador '{$name}' já existe no módulo '{$module}'.</error>");
return Command::FAILURE;
}
$namespace = "Modules\\" . ucfirst($module) . "\\Controllers";
$template = <<<PHP
<?php
namespace {$namespace};
use App\Controllers\BaseController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Kernel\View;
/**
* Controller {$name}
*/
class {$name} extends BaseController
{
/**
* Default action
*
* @param Request \$request
* @param Response \$response
* @return Response
*/
public function index(Request \$request, Response \$response): Response
{
// Sua lógica aqui
return \$response;
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Controlador '{$name}' criado com sucesso em modules/{$module}/classes/Controllers/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do controlador.</error>");
return Command::FAILURE;
}
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeExceptionCommand extends Command
{
protected static $defaultName = 'make:exception';
protected static $defaultDescription = 'Cria uma nova classe de exceção';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe de exceção (ex: InvalidPaymentException)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
if (!str_ends_with($name, 'Exception')) {
$name .= 'Exception';
}
$directory = dirname(__DIR__, 2) . '/Exceptions';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>A exceção '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace App\Exceptions;
use Exception;
class {$name} extends Exception
{
/**
* Exception logic here.
*/
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Exceção '{$name}' criada com sucesso em app/Exceptions/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo da exceção.</error>");
return Command::FAILURE;
}
}
}
+72
View File
@@ -0,0 +1,72 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeHelperCommand extends Command
{
protected static $defaultName = 'make:helper';
protected static $defaultDescription = 'Cria um novo arquivo de helper';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome do arquivo helper (ex: string_helpers)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
if (!str_ends_with($name, '.php')) {
$name .= '.php';
}
$directory = dirname(__DIR__, 2) . '/Helpers';
$filePath = $directory . '/' . $name;
if (file_exists($filePath)) {
$output->writeln("<error>O helper '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
if (!function_exists('your_function_name')) {
/**
* Helper description.
*/
function your_function_name()
{
//
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Helper '{$name}' criado com sucesso em app/Helpers/</info>");
$output->writeln("<comment>Lembre-se de incluir o arquivo no composer.json ou carregar manualmente.</comment>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do helper.</error>");
return Command::FAILURE;
}
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeInterfaceCommand extends Command
{
protected static $defaultName = 'make:interface';
protected static $defaultDescription = 'Cria uma nova interface PHP';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da interface (ex: PaymentInterface)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
if (!str_ends_with($name, 'Interface')) {
$name .= 'Interface';
}
$directory = dirname(__DIR__, 2) . '/Interfaces';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>A interface '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace App\Interfaces;
interface {$name}
{
/**
* Define interface methods.
*/
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Interface '{$name}' criada com sucesso em app/Interfaces/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo da interface.</error>");
return Command::FAILURE;
}
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeMiddlewareCommand extends Command
{
protected static $defaultName = 'make:middleware';
protected static $defaultDescription = 'Cria um novo middleware na aplicação';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe do middleware (ex: CheckAgeMiddleware)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
// Garante que o nome termine com Middleware para padronização
if (!str_ends_with($name, 'Middleware')) {
$name .= 'Middleware';
}
$directory = dirname(__DIR__, 2) . '/Middlewares';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O middleware '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$template = $this->getTemplate($name);
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Middleware '{$name}' criado com sucesso em app/Middlewares/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do middleware.</error>");
return Command::FAILURE;
}
}
protected function getTemplate(string $className): string
{
$year = date('Y');
return <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*/
namespace App\Middlewares;
use Closure;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class {$className}
{
/**
* Handle an incoming request.
*
* @param Request \$request
* @param Closure \$next
* @return mixed
*/
public function handle(Request \$request, Closure \$next)
{
// Lógica do middleware aqui
return \$next(\$request);
}
}
PHP;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeMigrationCommand extends Command
{
protected static $defaultName = 'make:migration';
protected static $defaultDescription = 'Creates a new migration file';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'The migration name (e.g. create_users_table)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
// Convert name to class format (CreateUsersTable)
$className = str_replace(' ', '', ucwords(str_replace('_', ' ', $name)));
// Generate timestamp for ordering
$timestamp = date('YmdHis');
// Define file name
$fileName = "{$timestamp}_{$name}.php";
// Migrations directory path
$migrationsPath = dirname(__DIR__, 3) . '/database/migrations';
if (!is_dir($migrationsPath)) {
mkdir($migrationsPath, 0755, true);
}
$filePath = $migrationsPath . '/' . $fileName;
// Migration template
$template = <<<PHP
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class {$className}
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
if (!Capsule::schema()->hasTable('table_name')) {
Capsule::schema()->create('table_name', function (Blueprint \$table) {
\$table->id();
\$table->timestamps();
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Capsule::schema()->dropIfExists('table_name');
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Migração criada com sucesso:</info> {$fileName}");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo de migração.</error>");
return Command::FAILURE;
}
}
}
+81
View File
@@ -0,0 +1,81 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Support\Str;
class MakeMigrationModuleCommand extends Command
{
protected static $defaultName = 'make:migration-module';
protected static $defaultDescription = 'Cria uma nova migração dentro de um módulo específico';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo')
->addArgument('name', InputArgument::REQUIRED, 'O nome da migração (ex: create_posts_table)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$module = strtolower($input->getArgument('module'));
$name = Str::snake($input->getArgument('name'));
$modulePath = dirname(__DIR__, 3) . "/modules/{$module}";
if (!is_dir($modulePath)) {
$output->writeln("<error>Módulo '{$module}' não encontrado.</error>");
return Command::FAILURE;
}
$migrationsPath = "{$modulePath}/database/migrations";
if (!is_dir($migrationsPath)) {
mkdir($migrationsPath, 0755, true);
}
// Busca o próximo número sequencial para o prefixo (SysCMS usa 001, 002...)
$existingFiles = glob("{$migrationsPath}/*.php");
$nextNumber = str_pad(count($existingFiles) + 1, 3, '0', STR_PAD_LEFT);
$fileName = "{$nextNumber}_{$name}.php";
$filePath = "{$migrationsPath}/{$fileName}";
$className = Str::studly($name);
$template = <<<PHP
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Database\Schema\Blueprint;
class {$className}
{
public function up()
{
Capsule::schema()->create('table_name', function (Blueprint \$table) {
\$table->bigIncrements('id');
// \$table->string('name');
\$table->timestamps();
});
}
public function down()
{
Capsule::schema()->dropIfExists('table_name');
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Migração criada com sucesso: modules/{$module}/database/migrations/{$fileName}</info>");
return Command::SUCCESS;
}
$output->writeln("<error>Falha ao criar o arquivo de migração.</error>");
return Command::FAILURE;
}
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeModelCommand extends Command
{
protected static $defaultName = 'make:model';
protected static $defaultDescription = 'Cria um novo modelo Eloquent em um módulo específico';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo (ex: blog)')
->addArgument('name', InputArgument::REQUIRED, 'O nome do modelo (ex: Post)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$module = strtolower($input->getArgument('module'));
$name = $input->getArgument('name');
$modulePath = dirname(__DIR__, 3) . '/modules/' . $module;
if (!is_dir($modulePath)) {
$output->writeln("<error>O módulo '{$module}' não existe.</error>");
return Command::FAILURE;
}
$modelsPath = $modulePath . '/classes/Models';
if (!is_dir($modelsPath)) {
mkdir($modelsPath, 0755, true);
}
$filePath = $modelsPath . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O modelo '{$name}' já existe no módulo '{$module}'.</error>");
return Command::FAILURE;
}
$namespace = "Modules\\" . ucfirst($module) . "\\Models";
$tableName = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name)) . 's';
$template = <<<PHP
<?php
namespace {$namespace};
use Illuminate\Database\Eloquent\Model;
/**
* Model {$name}
*/
class {$name} extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected \$table = '{$tableName}';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected \$fillable = [
// Adicione os campos aqui
];
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public \$timestamps = true;
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Modelo '{$name}' criado com sucesso em modules/{$module}/classes/Models/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do modelo.</error>");
return Command::FAILURE;
}
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeModuleCommand extends Command
{
protected static $defaultName = 'make:module';
protected static $defaultDescription = 'Cria a estrutura básica de um novo módulo';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome do módulo (ex: blog)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = strtolower($input->getArgument('name'));
$modulePath = dirname(__DIR__, 3) . '/modules/' . $name;
if (is_dir($modulePath)) {
$output->writeln("<error>O módulo '{$name}' já existe.</error>");
return Command::FAILURE;
}
// Criar estrutura de diretórios
$directories = [
$modulePath,
$modulePath . '/routes',
$modulePath . '/views',
$modulePath . '/classes',
$modulePath . '/classes/Controllers',
$modulePath . '/classes/Models',
];
foreach ($directories as $dir) {
if (!mkdir($dir, 0755, true)) {
$output->writeln("<error>Falha ao criar diretório: {$dir}</error>");
return Command::FAILURE;
}
}
// Criar module.json
$moduleJson = [
'name' => ucfirst($name),
'version' => '1.0.0',
'description' => 'Descrição do módulo ' . ucfirst($name),
'author' => 'SysCMS',
'enabled' => true
];
file_put_contents($modulePath . '/module.json', json_encode($moduleJson, JSON_PRETTY_PRINT));
// Criar rotas web.php
$webRoutes = <<<PHP
<?php
/** @var \Slim\App \$app */
\$app->group('/{$name}', function (\$group) {
\$group->get('', [\Modules\\ucfirst($name)\Controllers\\ucfirst($name)Controller::class, 'index']);
});
PHP;
file_put_contents($modulePath . '/routes/web.php', $webRoutes);
// Criar rotas api.php
$apiRoutes = "<?php\n\n/** @var \Slim\App \$app */\n";
file_put_contents($modulePath . '/routes/api.php', $apiRoutes);
// Criar Controlador inicial
$controllerName = ucfirst($name) . 'Controller';
$namespace = "Modules\\" . ucfirst($name) . "\\Controllers";
$controllerTemplate = <<<PHP
<?php
namespace {$namespace};
use App\Controllers\BaseController;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Kernel\View;
/**
* Controller for {$name} module
*/
class {$controllerName} extends BaseController
{
/**
* Index action
*
* @param Request \$request
* @param Response \$response
* @return Response
*/
public function index(Request \$request, Response \$response): Response
{
return View::render(\$response, '@{$name}/index.twig', [
'title' => 'Módulo ' . ucfirst('{$name}')
]);
}
}
PHP;
file_put_contents($modulePath . '/classes/Controllers/' . $controllerName . '.php', $controllerTemplate);
// Criar view inicial
$viewTemplate = <<<HTML
{% extends "layouts/dashboard.twig" %}
{% block content %}
<div class="container-fluid">
<h1 class="mt-4">{{ title }}</h1>
<div class="card mb-4">
<div class="card-body">
Este é o módulo {{ title }} gerado automaticamente.
</div>
</div>
</div>
{% endblock %}
HTML;
file_put_contents($modulePath . '/views/index.twig', $viewTemplate);
$output->writeln("<info>Módulo '{$name}' criado com sucesso em modules/{$name}</info>");
return Command::SUCCESS;
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeRepositoryCommand extends Command
{
protected static $defaultName = 'make:repository';
protected static $defaultDescription = 'Cria uma nova classe de repositório';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe de repositório (ex: UserRepository)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
if (!str_ends_with($name, 'Repository')) {
$name .= 'Repository';
}
$directory = dirname(__DIR__, 2) . '/Repositories';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O repositório '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace App\Repositories;
class {$name}
{
/**
* Repository logic here.
*/
public function all()
{
// Return all records
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Repositório '{$name}' criado com sucesso em app/Repositories/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do repositório.</error>");
return Command::FAILURE;
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeSeedCommand extends Command
{
protected static $defaultName = 'make:seed';
protected static $defaultDescription = 'Cria uma nova classe de seeder';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe seeder (ex: UserSeeder)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
// Garantir que termina com Seeder
if (!str_ends_with($name, 'Seeder')) {
$name .= 'Seeder';
}
$seedersPath = dirname(__DIR__, 3) . '/database/seeders';
if (!is_dir($seedersPath)) {
mkdir($seedersPath, 0755, true);
}
$filePath = $seedersPath . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O seeder '{$name}' já existe.</error>");
return Command::FAILURE;
}
$template = <<<PHP
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
class {$name}
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
// Capsule::table('users')->insert([
// 'user_name' => 'Admin',
// 'user_email' => 'admin@example.com',
// 'user_password' => password_hash('password', PASSWORD_DEFAULT),
// ]);
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Seeder criado com sucesso:</info> database/seeders/{$name}.php");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do seeder.</error>");
return Command::FAILURE;
}
}
}
+74
View File
@@ -0,0 +1,74 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeServiceCommand extends Command
{
protected static $defaultName = 'make:service';
protected static $defaultDescription = 'Cria uma nova classe de serviço';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da classe de serviço (ex: PaymentService)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
if (!str_ends_with($name, 'Service')) {
$name .= 'Service';
}
$directory = dirname(__DIR__, 2) . '/Services';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>O serviço '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace App\Services;
class {$name}
{
/**
* Service logic here.
*/
public function execute()
{
// Implement logic
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Serviço '{$name}' criado com sucesso em app/Services/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do serviço.</error>");
return Command::FAILURE;
}
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class MakeTraitCommand extends Command
{
protected static $defaultName = 'make:trait';
protected static $defaultDescription = 'Cria uma nova Trait PHP';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome da trait (ex: HasUuid)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
$directory = dirname(__DIR__, 2) . '/Traits';
$filePath = $directory . '/' . $name . '.php';
if (file_exists($filePath)) {
$output->writeln("<error>A trait '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace App\Traits;
trait {$name}
{
/**
* Trait logic here.
*/
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Trait '{$name}' criada com sucesso em app/Traits/</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo da trait.</error>");
return Command::FAILURE;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
class MakeViewCommand extends Command
{
protected static $defaultName = 'make:view';
protected static $defaultDescription = 'Cria um novo template Twig';
protected function configure(): void
{
$this
->addArgument('name', InputArgument::REQUIRED, 'O nome do template (ex: users/index)')
->addOption('module', 'm', InputOption::VALUE_OPTIONAL, 'O módulo onde o template será criado');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$name = $input->getArgument('name');
$module = $input->getOption('module');
// Garante a extensão .twig
if (!str_ends_with($name, '.twig')) {
$name .= '.twig';
}
if ($module) {
$directory = dirname(__DIR__, 3) . "/modules/{$module}/views";
} else {
$directory = dirname(__DIR__, 3) . "/templates";
}
$filePath = $directory . '/' . $name;
$fileDir = dirname($filePath);
if (file_exists($filePath)) {
$output->writeln("<error>O template '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($fileDir)) {
mkdir($fileDir, 0755, true);
}
$template = "{% extends \"layouts/base.twig\" %}\n\n{% block content %}\n<div class=\"container mt-4\">\n <h1>" . ucfirst(basename($name, '.twig')) . "</h1>\n <p>Novo template criado via CLI.</p>\n</div>\n{% endblock %}\n";
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Template '{$name}' criado com sucesso em " . ($module ? "modules/{$module}/views/" : "templates/") . "</info>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do template.</error>");
return Command::FAILURE;
}
}
}
+144
View File
@@ -0,0 +1,144 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
class MigrateCommand extends Command
{
protected static $defaultName = 'migrate';
protected static $defaultDescription = 'Executes database migrations';
protected function configure(): void
{
$this
->setHelp('This command checks and executes all pending database migrations.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln([
'Iniciando execução das migrations...',
'=====================================',
'',
]);
// Ensure migrations table exists
$this->ensureMigrationsTableExists();
$migrationsPath = dirname(__DIR__, 3) . '/database/migrations';
$modulesPath = dirname(__DIR__, 3) . '/modules';
$migrationFiles = [];
// Core migrations
if (is_dir($migrationsPath)) {
$coreFiles = glob($migrationsPath . '/*.php');
if ($coreFiles) {
$migrationFiles = array_merge($migrationFiles, $coreFiles);
}
} else {
$output->writeln("<error>Diretório de migrations não encontrado: $migrationsPath</error>");
}
// Module migrations
if (is_dir($modulesPath)) {
$moduleFiles = glob($modulesPath . '/*/database/migrations/*.php');
if ($moduleFiles) {
$migrationFiles = array_merge($migrationFiles, $moduleFiles);
}
}
// Sort migration files by filename (keeping chronological order if timestamp/numbering exists)
usort($migrationFiles, function($a, $b) {
return basename($a) <=> basename($b);
});
if (empty($migrationFiles)) {
$output->writeln('<info>Nenhuma migração encontrada.</info>');
return Command::SUCCESS;
}
// Get already executed migrations
$executedMigrations = Capsule::table('migrations')->pluck('migration')->toArray();
$nextBatch = Capsule::table('migrations')->max('batch') + 1;
$count = 0;
foreach ($migrationFiles as $file) {
$fileName = basename($file, '.php');
// Skip if already executed
if (in_array($fileName, $executedMigrations)) {
continue;
}
// Remove numeric prefix (e.g. 001_)
$cleanName = preg_replace('/^\d+_/', '', $fileName);
// Convert snake_case to PascalCase (e.g. create_bus_table -> CreateBusTable)
$className = str_replace(' ', '', ucwords(str_replace('_', ' ', $cleanName)));
$output->write("Executando migration: <info>{$fileName}</info>... ");
try {
require_once $file;
if (class_exists($className)) {
$migration = new $className();
// Check if up method exists
if (method_exists($migration, 'up')) {
$migration->up();
// Record migration as executed
Capsule::table('migrations')->insert([
'migration' => $fileName,
'batch' => $nextBatch
]);
$output->writeln('<info>OK</info>');
$count++;
} else {
$output->writeln('<comment>SKIPPED (método up não encontrado)</comment>');
}
} else {
$output->writeln("<error>ERRO (Classe {$className} não encontrada)</error>");
}
} catch (\Exception $e) {
$output->writeln("<error>FALHA</error>");
$output->writeln("<error>Erro: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
if ($count === 0) {
$output->writeln('<info>Nada para migrar.</info>');
} else {
$output->writeln([
'',
"<info>Total de {$count} migration(s) processada(s) com sucesso!</info>",
]);
}
return Command::SUCCESS;
}
/**
* Ensure the migrations table exists in the database.
*
* @return void
*/
protected function ensureMigrationsTableExists(): void
{
if (!Capsule::schema()->hasTable('migrations')) {
Capsule::schema()->create('migrations', function ($table) {
$table->increments('id');
$table->string('migration');
$table->integer('batch');
});
}
}
}
+46
View File
@@ -0,0 +1,46 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\ArrayInput;
class MigrateFreshCommand extends Command
{
protected static $defaultName = 'migrate:fresh';
protected static $defaultDescription = 'Drop all tables and re-run all migrations';
protected function configure(): void
{
$this
->addOption('seed', null, InputOption::VALUE_NONE, 'Indicates if the seed command should be re-run');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('<info>Iniciando limpeza do banco de dados (fresh)...</info>');
// 1. Wipe database
$wipeCommand = $this->getApplication()->find('db:wipe');
$wipeInput = new ArrayInput(['--force' => true]);
$wipeCommand->run($wipeInput, $output);
// 2. Run migrations
$migrateCommand = $this->getApplication()->find('migrate');
$migrateInput = new ArrayInput([]);
$migrateCommand->run($migrateInput, $output);
// 3. Optional seed
if ($input->getOption('seed')) {
$seedCommand = $this->getApplication()->find('db:seed');
$seedInput = new ArrayInput([]);
$seedCommand->run($seedInput, $output);
}
$output->writeln('<info>Operação migrate:fresh concluída com sucesso!</info>');
return Command::SUCCESS;
}
}
+57
View File
@@ -0,0 +1,57 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\ArrayInput;
class MigrateRefreshCommand extends Command
{
protected static $defaultName = 'migrate:refresh';
protected static $defaultDescription = 'Reset and re-run all migrations';
protected function configure(): void
{
$this
->addOption('step', 's', InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted', 0)
->addOption('seed', null, InputOption::VALUE_NONE, 'Indicates if the seed command should be re-run');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$step = (int) $input->getOption('step');
if ($step > 0) {
$output->writeln("<info>Revertendo {$step} passos de migração...</info>");
$rollbackCommand = $this->getApplication()->find('migrate:rollback');
$rollbackInput = new ArrayInput(['--step' => $step]);
$rollbackCommand->run($rollbackInput, $output);
} else {
$output->writeln('<info>Revertendo todas as migrações (reset)...</info>');
// Reset all by calling rollback in loop or a specific reset command
// Assuming we'll use rollback in loop for simplicity or adding a reset later
// Here we use step=999 as a convention for "all" in our rollback implementation
$rollbackCommand = $this->getApplication()->find('migrate:rollback');
$rollbackInput = new ArrayInput(['--step' => 999]);
$rollbackCommand->run($rollbackInput, $output);
}
$output->writeln('<info>Executando migrações novamente...</info>');
$migrateCommand = $this->getApplication()->find('migrate');
$migrateInput = new ArrayInput([]);
$migrateCommand->run($migrateInput, $output);
if ($input->getOption('seed')) {
$output->writeln('<info>Executando seeders...</info>');
$seedCommand = $this->getApplication()->find('db:seed');
$seedInput = new ArrayInput([]);
$seedCommand->run($seedInput, $output);
}
$output->writeln('<info>Operação migrate:refresh concluída com sucesso!</info>');
return Command::SUCCESS;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Illuminate\Database\Capsule\Manager as Capsule;
class MigrationStatusCommand extends Command
{
protected static $defaultName = 'migrate:status';
protected static $defaultDescription = 'Mostra o status de todas as migrações';
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!Capsule::schema()->hasTable('migrations')) {
$output->writeln('<error>Tabela de migrations não encontrada.</error>');
return Command::FAILURE;
}
$migrationsPath = dirname(__DIR__, 3) . '/database/migrations';
$modulesPath = dirname(__DIR__, 3) . '/modules';
$migrationFiles = [];
// Core migrations
if (is_dir($migrationsPath)) {
$coreFiles = glob($migrationsPath . '/*.php');
if ($coreFiles) {
$migrationFiles = array_merge($migrationFiles, $coreFiles);
}
}
// Module migrations
if (is_dir($modulesPath)) {
$moduleFiles = glob($modulesPath . '/*/database/migrations/*.php');
if ($moduleFiles) {
$migrationFiles = array_merge($migrationFiles, $moduleFiles);
}
}
// Sort migration files
usort($migrationFiles, function($a, $b) {
return basename($a) <=> basename($b);
});
// Get executed migrations
$executedMigrations = Capsule::table('migrations')->get()->keyBy('migration');
$table = new Table($output);
$table->setHeaders(['Executada?', 'Migration', 'Batch']);
foreach ($migrationFiles as $file) {
$fileName = basename($file, '.php');
$executed = $executedMigrations->has($fileName);
$table->addRow([
$executed ? '<info>Sim</info>' : '<comment>Não</comment>',
$fileName,
$executed ? $executedMigrations->get($fileName)->batch : '-'
]);
}
$table->render();
return Command::SUCCESS;
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Illuminate\Database\Capsule\Manager as Capsule;
class ModuleDeleteCommand extends Command
{
protected static $defaultName = 'module:delete';
protected static $defaultDescription = 'Remove fisicamente um módulo e suas tabelas associadas';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo a ser removido')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Confirmar a remoção sem prompt');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$moduleName = $input->getArgument('module');
$force = $input->getOption('force');
$modulePath = dirname(__DIR__, 3) . '/modules/' . $moduleName;
if (!is_dir($modulePath)) {
$output->writeln("<error>Módulo '{$moduleName}' não encontrado em modules/.</error>");
return Command::FAILURE;
}
if (!$force) {
$output->writeln("<comment>Você tem certeza que deseja remover o módulo '{$moduleName}' e TODAS as suas tabelas associadas?</comment>");
$output->writeln("<info>Use a opção --force para confirmar.</info>");
return Command::FAILURE;
}
// 1. Drop associated tables if listed in module.json
$jsonPath = $modulePath . '/module.json';
if (file_exists($jsonPath)) {
$config = json_decode(file_get_contents($jsonPath), true);
if (isset($config['tables']) && is_array($config['tables'])) {
$output->writeln("<info>Limpando tabelas do banco de dados...</info>");
Capsule::connection()->statement('SET FOREIGN_KEY_CHECKS = 0');
foreach ($config['tables'] as $table) {
$output->write("Dropando tabela: <comment>{$table}</comment>... ");
Capsule::schema()->dropIfExists($table);
$output->writeln('<info>OK</info>');
}
Capsule::connection()->statement('SET FOREIGN_KEY_CHECKS = 1');
}
}
// 2. Remove files
$output->writeln("<info>Removendo arquivos do módulo...</info>");
$this->deleteDirectory($modulePath);
$output->writeln("<info>Módulo '{$moduleName}' removido com sucesso!</info>");
return Command::SUCCESS;
}
protected function deleteDirectory($dir): bool
{
if (!file_exists($dir)) {
return true;
}
if (!is_dir($dir)) {
return unlink($dir);
}
foreach (scandir($dir) as $item) {
if ($item == '.' || $item == '..') {
continue;
}
if (!$this->deleteDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
return false;
}
}
return rmdir($dir);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ModuleDisableCommand extends Command
{
protected static $defaultName = 'module:disable';
protected static $defaultDescription = 'Desativa um módulo no arquivo module.json';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo a ser desativado');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$moduleName = $input->getArgument('module');
$modulePath = dirname(__DIR__, 3) . '/modules/' . $moduleName . '/module.json';
if (!file_exists($modulePath)) {
$output->writeln("<error>Módulo '{$moduleName}' não encontrado ou arquivo module.json inexistente.</error>");
return Command::FAILURE;
}
$config = json_decode(file_get_contents($modulePath), true);
if ($config === null) {
$output->writeln("<error>Erro ao ler o arquivo module.json do módulo '{$moduleName}'.</error>");
return Command::FAILURE;
}
$isActive = $config['module_active'] ?? $config['module_ative'] ?? false;
if ($isActive === false) {
$output->writeln("<comment>O módulo '{$moduleName}' já está desativado.</comment>");
return Command::SUCCESS;
}
// Maintain the key already used in the file if possible
if (isset($config['module_ative'])) {
$config['module_ative'] = false;
} else {
$config['module_active'] = false;
}
if (file_put_contents($modulePath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))) {
$output->writeln("<info>Módulo '{$moduleName}' desativado com sucesso!</info>");
return Command::SUCCESS;
}
$output->writeln("<error>Falha ao atualizar o arquivo module.json do módulo '{$moduleName}'.</error>");
return Command::FAILURE;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ModuleEnableCommand extends Command
{
protected static $defaultName = 'module:enable';
protected static $defaultDescription = 'Ativa um módulo no arquivo module.json';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo a ser ativado');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$moduleName = $input->getArgument('module');
$modulePath = dirname(__DIR__, 3) . '/modules/' . $moduleName . '/module.json';
if (!file_exists($modulePath)) {
$output->writeln("<error>Módulo '{$moduleName}' não encontrado ou arquivo module.json inexistente.</error>");
return Command::FAILURE;
}
$config = json_decode(file_get_contents($modulePath), true);
if ($config === null) {
$output->writeln("<error>Erro ao ler o arquivo module.json do módulo '{$moduleName}'.</error>");
return Command::FAILURE;
}
$isActive = $config['module_active'] ?? $config['module_ative'] ?? false;
if ($isActive === true) {
$output->writeln("<comment>O módulo '{$moduleName}' já está ativado.</comment>");
return Command::SUCCESS;
}
// Maintain the key already used in the file if possible
if (isset($config['module_ative'])) {
$config['module_ative'] = true;
} else {
$config['module_active'] = true;
}
if (file_put_contents($modulePath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))) {
$output->writeln("<info>Módulo '{$moduleName}' ativado com sucesso!</info>");
return Command::SUCCESS;
}
$output->writeln("<error>Falha ao atualizar o arquivo module.json do módulo '{$moduleName}'.</error>");
return Command::FAILURE;
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Illuminate\Database\Capsule\Manager as Capsule;
use Kernel\ModuleLoader;
class ModuleListCommand extends Command
{
protected static $defaultName = 'module:list';
protected static $defaultDescription = 'Lista todos os módulos instalados e suas informações';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$modulesPath = dirname(__DIR__, 3) . '/modules';
$loader = new ModuleLoader($modulesPath);
try {
// Get all directories in modules/
$dirs = glob($modulesPath . '/*', GLOB_ONLYDIR);
if (empty($dirs)) {
$output->writeln('<info>Nenhum módulo encontrado em modules/.</info>');
return Command::SUCCESS;
}
$output->writeln('<info>Módulos do Sistema:</info>');
$table = new Table($output);
$table->setHeaders(['Nome', 'Versão', 'Ativo', 'Pronto', 'DB', 'Descrição']);
// Get registered modules from DB using correct column name
$dbModules = [];
if (Capsule::schema()->hasTable('modules')) {
$dbModules = Capsule::table('modules')->pluck('modules_name')->toArray();
}
foreach ($dirs as $dir) {
$name = basename($dir);
$jsonFile = $dir . '/module.json';
$version = 'N/A';
$active = '<comment>Não</comment>';
$ready = '<comment>Não</comment>';
$dbStatus = in_array($name, $dbModules) ? '<info>Sim</info>' : '<comment>Não</comment>';
$description = 'Sem descrição';
if (file_exists($jsonFile)) {
$config = json_decode(file_get_contents($jsonFile), true);
if ($config) {
$version = $config['version'] ?? '1.0.0';
// Check both module_active and module_ative, default to true if missing
$isActive = $config['module_active'] ?? $config['module_ative'] ?? true;
$active = $isActive ? '<info>Sim</info>' : '<comment>Não</comment>';
$isReady = $config['ready'] ?? true;
$ready = $isReady ? '<info>Sim</info>' : '<comment>Não</comment>';
$description = $config['description'] ?? 'Sem descrição';
}
}
$table->addRow([
$name,
$version,
$active,
$ready,
$dbStatus,
substr($description, 0, 40) . (strlen($description) > 40 ? '...' : '')
]);
}
$table->render();
$output->writeln("<info>Total de módulos: " . count($dirs) . "</info>");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln('<error>Erro ao listar módulos: ' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ModuleMakeProviderCommand extends Command
{
protected static $defaultName = 'module:make-provider';
protected static $defaultDescription = 'Cria um novo Service Provider para um módulo específico';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo')
->addArgument('name', InputArgument::REQUIRED, 'O nome do Service Provider (ex: EventServiceProvider)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$moduleName = $input->getArgument('module');
$name = $input->getArgument('name');
if (!str_ends_with($name, 'ServiceProvider')) {
$name .= 'ServiceProvider';
}
$modulePath = dirname(__DIR__, 3) . '/modules/' . $moduleName;
$directory = $modulePath . '/Providers';
$filePath = $directory . '/' . $name . '.php';
if (!is_dir($modulePath)) {
$output->writeln("<error>Módulo '{$moduleName}' não encontrado.</error>");
return Command::FAILURE;
}
if (file_exists($filePath)) {
$output->writeln("<error>O Service Provider '{$name}' já existe!</error>");
return Command::FAILURE;
}
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
$year = date('Y');
$moduleNamespace = "Modules\\" . ucfirst($moduleName);
$template = <<<PHP
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) {$year} Marco Costa
* All rights reserved.
*/
namespace {$moduleNamespace}\Providers;
class {$name}
{
/**
* Register the service provider.
*/
public function register()
{
// Register services in the container
}
/**
* Boot the service provider.
*/
public function boot()
{
// Boot logic
}
}
PHP;
if (file_put_contents($filePath, $template)) {
$output->writeln("<info>Service Provider '{$name}' criado com sucesso em modules/{$moduleName}/Providers/</info>");
$output->writeln("<comment>Lembre-se de registrar o provider no arquivo module.json se necessário.</comment>");
return Command::SUCCESS;
} else {
$output->writeln("<error>Falha ao criar o arquivo do Service Provider.</error>");
return Command::FAILURE;
}
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ModulePublishCommand extends Command
{
protected static $defaultName = 'module:publish';
protected static $defaultDescription = 'Copia os assets de um módulo para a pasta public do sistema';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do módulo');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$module = strtolower($input->getArgument('module'));
$modulePath = dirname(__DIR__, 3) . "/modules/{$module}";
if (!is_dir($modulePath)) {
$output->writeln("<error>Módulo '{$module}' não encontrado.</error>");
return Command::FAILURE;
}
$assetsPath = "{$modulePath}/assets";
if (!is_dir($assetsPath)) {
$output->writeln("<comment>Nenhum diretório 'assets' encontrado no módulo '{$module}'.</comment>");
return Command::SUCCESS;
}
$publicPath = dirname(__DIR__, 3) . "/public/assets/modules/{$module}";
if (!is_dir($publicPath)) {
mkdir($publicPath, 0755, true);
}
$output->writeln("<info>Copiando assets de '{$module}' para 'public/assets/modules/{$module}'...</info>");
$this->copyDirectory($assetsPath, $publicPath);
$output->writeln("<info>Assets publicados com sucesso!</info>");
return Command::SUCCESS;
}
protected function copyDirectory($source, $destination)
{
$dir = opendir($source);
@mkdir($destination);
while (false !== ($file = readdir($dir))) {
if (($file != '.') && ($file != '..')) {
if (is_dir($source . '/' . $file)) {
$this->copyDirectory($source . '/' . $file, $destination . '/' . $file);
} else {
copy($source . '/' . $file, $destination . '/' . $file);
}
}
}
closedir($dir);
}
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
class ModuleRegisterCommand extends Command
{
protected static $defaultName = 'module:register';
protected static $defaultDescription = 'Registra um módulo existente na tabela modules do banco de dados';
protected function configure(): void
{
$this
->addArgument('module', InputArgument::REQUIRED, 'O nome do diretório do módulo');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$moduleName = $input->getArgument('module');
$modulePath = dirname(__DIR__, 3) . '/modules/' . $moduleName . '/module.json';
if (!file_exists($modulePath)) {
$output->writeln("<error>Módulo '{$moduleName}' não encontrado ou arquivo module.json inexistente.</error>");
return Command::FAILURE;
}
$config = json_decode(file_get_contents($modulePath), true);
if ($config === null) {
$output->writeln("<error>Erro ao processar o arquivo module.json do módulo '{$moduleName}'.</error>");
return Command::FAILURE;
}
// Verifica se a tabela modules existe
if (!Capsule::schema()->hasTable('modules')) {
$output->writeln("<error>A tabela 'modules' não existe no banco de dados. Execute as migrations primeiro.</error>");
return Command::FAILURE;
}
// Verifica se o módulo já está registrado usando a coluna correta
$existing = Capsule::table('modules')->where('modules_name', $moduleName)->first();
$data = [
'modules_name' => $moduleName,
'modules_display_name' => $config['display_name'] ?? $moduleName,
'modules_description' => $config['description'] ?? '',
'modules_version' => $config['version'] ?? '1.0.0',
'modules_ative' => $config['module_active'] ?? $config['module_ative'] ?? true,
'updated_at' => date('Y-m-d H:i:s')
];
if ($existing) {
Capsule::table('modules')->where('modules_name', $moduleName)->update($data);
$output->writeln("<info>Registro do módulo '{$moduleName}' atualizado com sucesso!</info>");
} else {
$data['created_at'] = date('Y-m-d H:i:s');
Capsule::table('modules')->insert($data);
$output->writeln("<info>Módulo '{$moduleName}' registrado com sucesso na tabela modules!</info>");
}
return Command::SUCCESS;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class ModuleTestCommand extends Command
{
protected static $defaultName = 'module:test';
protected function configure()
{
$this
->setDescription('Executa testes automatizados para um módulo específico')
->addArgument('name', InputArgument::REQUIRED, 'Nome do módulo');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$moduleName = strtolower($input->getArgument('name'));
$modulePath = dirname(dirname(dirname(__DIR__))) . '/modules/' . $moduleName;
if (!is_dir($modulePath)) {
$io->error("Módulo '{$moduleName}' não encontrado em {$modulePath}");
return Command::FAILURE;
}
$testPath = $modulePath . '/tests';
if (!is_dir($testPath)) {
$io->warning("O módulo '{$moduleName}' não possui uma pasta de testes em {$testPath}.");
return Command::INVALID;
}
$io->title("Iniciando testes para o módulo: " . ucfirst($moduleName));
// Execute PHPUnit pointing to the module's test directory
// Using vendor/bin/phpunit which is standard for the project
$phpunitExec = dirname(dirname(dirname(__DIR__))) . '/vendor/bin/phpunit';
// We use the full path to phpunit to avoid command not found issues
$command = "php \"{$phpunitExec}\" \"{$testPath}\"";
$io->note("Executando: {$command}");
passthru($command, $exitCode);
if ($exitCode === 0) {
$io->success("Todos os testes do módulo '{$moduleName}' passaram!");
} else {
$io->error("Falha nos testes do módulo '{$moduleName}'.");
}
return $exitCode === 0 ? Command::SUCCESS : Command::FAILURE;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class OpcacheClearCommand extends Command
{
protected static $defaultName = 'opcache:clear';
protected static $defaultDescription = 'Limpa o cache do OPcache (PHP)';
protected function configure(): void
{
$this->setHelp('Este comando executa a função opcache_reset() para invalidar todo o cache de bytecode.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!function_exists('opcache_reset')) {
$output->writeln('<error>OPcache não está habilitado ou a função opcache_reset() não está disponível.</error>');
return Command::FAILURE;
}
if (opcache_reset()) {
$output->writeln('<info>OPcache limpo com sucesso!</info>');
return Command::SUCCESS;
}
$output->writeln('<error>Falha ao limpar o OPcache.</error>');
return Command::FAILURE;
}
}
+48
View File
@@ -0,0 +1,48 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Illuminate\Database\Capsule\Manager as Capsule;
class PermissionListCommand extends Command
{
protected static $defaultName = 'permission:list';
protected static $defaultDescription = 'Lista todas as permissões cadastradas no sistema';
protected function execute(InputInterface $input, OutputInterface $output): int
{
if (!Capsule::schema()->hasTable('permissions')) {
$output->writeln('<error>Tabela de permissões não encontrada.</error>');
return Command::FAILURE;
}
$permissions = Capsule::table('permissions')
->orderBy('permission_name')
->get();
if ($permissions->isEmpty()) {
$output->writeln('<info>Nenhuma permissão encontrada.</info>');
return Command::SUCCESS;
}
$table = new Table($output);
$table->setHeaders(['ID', 'Nome', 'Slug', 'Descrição']);
foreach ($permissions as $permission) {
$table->addRow([
$permission->permission_id,
$permission->permission_name,
$permission->permission_slug,
$permission->permission_description ?? '-'
]);
}
$table->render();
return Command::SUCCESS;
}
}
+130
View File
@@ -0,0 +1,130 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Illuminate\Database\Capsule\Manager as Capsule;
class RollbackCommand extends Command
{
protected static $defaultName = 'migrate:rollback';
protected static $defaultDescription = 'Reverte a última leva de migrações do banco de dados';
protected function configure(): void
{
$this
->setHelp('Este comando reverte as migrações que foram executadas na última operação de migração.')
->addOption('step', 's', \Symfony\Component\Console\Input\InputOption::VALUE_OPTIONAL, 'The number of migrations to be reverted', 0);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$step = (int) $input->getOption('step');
$output->writeln([
'Iniciando rollback das migrations...',
'=====================================',
'',
]);
if (!Capsule::schema()->hasTable('migrations')) {
$output->writeln('<error>Tabela de migrations não encontrada.</error>');
return Command::FAILURE;
}
if ($step === 999) {
// Special case for migrate:reset or fresh
$migrationsToRollback = Capsule::table('migrations')
->orderBy('id', 'desc')
->get();
} elseif ($step > 0) {
// Revert specific number of migrations
$migrationsToRollback = Capsule::table('migrations')
->orderBy('id', 'desc')
->limit($step)
->get();
} else {
// Pegar o último batch
$lastBatch = Capsule::table('migrations')->max('batch');
if (!$lastBatch) {
$output->writeln('<info>Nenhuma migração para reverter.</info>');
return Command::SUCCESS;
}
$migrationsToRollback = Capsule::table('migrations')
->where('batch', $lastBatch)
->orderBy('id', 'desc')
->get();
}
if ($migrationsToRollback->isEmpty()) {
$output->writeln('<info>Nenhuma migração para reverter.</info>');
return Command::SUCCESS;
}
$migrationsPath = dirname(__DIR__, 3) . '/database/migrations';
$modulesPath = dirname(__DIR__, 3) . '/modules';
foreach ($migrationsToRollback as $row) {
$fileName = $row->migration;
$file = $this->findMigrationFile($fileName, $migrationsPath, $modulesPath);
if (!$file) {
$output->writeln("<error>Arquivo de migração não encontrado: {$fileName}.php</error>");
continue;
}
// Converter nome para classe
$cleanName = preg_replace('/^\d+_/', '', $fileName);
$className = str_replace(' ', '', ucwords(str_replace('_', ' ', $cleanName)));
$output->write("Revertendo migration: <info>{$fileName}</info>... ");
try {
require_once $file;
if (class_exists($className)) {
$migration = new $className();
if (method_exists($migration, 'down')) {
$migration->down();
// Remover registro da tabela de migrations
Capsule::table('migrations')->where('id', $row->id)->delete();
$output->writeln('<info>OK</info>');
} else {
$output->writeln('<comment>Método down() não encontrado.</comment>');
}
} else {
$output->writeln("<error>Classe {$className} não encontrada.</error>");
}
} catch (\Exception $e) {
$output->writeln("<error>Erro: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
return Command::SUCCESS;
}
protected function findMigrationFile(string $name, string $corePath, string $modulesPath): ?string
{
// Procurar no core
$coreFile = $corePath . '/' . $name . '.php';
if (file_exists($coreFile)) {
return $coreFile;
}
// Procurar nos módulos
$moduleFiles = glob($modulesPath . '/*/database/migrations/' . $name . '.php');
if ($moduleFiles && count($moduleFiles) > 0) {
return $moduleFiles[0];
}
return null;
}
}
+77
View File
@@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Helper\Table;
use Kernel\Application;
use Kernel\ModuleLoader;
class RouteListCommand extends Command
{
protected static $defaultName = 'route:list';
protected function configure(): void
{
$this->setDescription('Lists all routes registered in the application.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
// We need to instantiate the application to load the routes
// But be careful not to execute session_start() if it is already active or causes output
// Simulate application environment
if (session_status() == PHP_SESSION_NONE) {
// Avoid headers error if something attempts to start session
// But Application initSession attempts to start.
// Let's suppress session errors for the command
@session_start();
}
$app = new Application();
// The Application constructor already loads routes via initRouter and initModuleLoader
// But we must ensure it loads everything that adds routes
// Since Application is a singleton, we can try to get the instance or create it
// If we create new Application(), it runs the constructor.
$routes = $app->getRoutes();
if (empty($routes)) {
$output->writeln('<info>Nenhuma rota encontrada.</info>');
return Command::SUCCESS;
}
$table = new Table($output);
$table->setHeaders(['Método', 'URI', 'Handler']);
foreach ($routes as $route) {
[$method, $uri, $handler] = $route;
$handlerName = 'Closure';
if (is_array($handler)) {
if (is_object($handler[0])) {
$handlerName = get_class($handler[0]) . '@' . $handler[1];
} elseif (is_string($handler[0])) {
$handlerName = $handler[0] . '@' . $handler[1];
}
} elseif (is_string($handler)) {
$handlerName = $handler;
}
$table->addRow([
$method,
$uri,
$handlerName
]);
}
$table->render();
return Command::SUCCESS;
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class StorageLinkCommand extends Command
{
protected static $defaultName = 'storage:link';
protected static $defaultDescription = 'Create the symbolic link from "public/storage" to "storage/uploads"';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$root = dirname(__DIR__, 3);
$target = $root . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'uploads';
$link = $root . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR . 'storage';
if (!is_dir($target)) {
if (!mkdir($target, 0755, true)) {
$output->writeln('<error>Could not create directory: ' . $target . '</error>');
return Command::FAILURE;
}
$output->writeln('<info>Created directory: ' . $target . '</info>');
}
if (file_exists($link) || is_link($link)) {
$output->writeln('<error>The "public/storage" already exists (as a file, directory or link).</error>');
return Command::FAILURE;
}
if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
// On Windows, try Junction first as it doesn't require admin privileges
$command = sprintf('mklink /J "%s" "%s"', $link, $target);
exec($command, $cmdOutput, $returnVar);
if ($returnVar === 0) {
$output->writeln('<info>The [public/storage] link has been connected to [storage/uploads] (Junction).</info>');
return Command::SUCCESS;
}
// Fallback to symlink if junction failed (though unlikely)
if (@symlink($target, $link)) {
$output->writeln('<info>The [public/storage] link has been connected to [storage/uploads] (Symlink).</info>');
return Command::SUCCESS;
}
} else {
// On Linux/Unix
if (@symlink($target, $link)) {
$output->writeln('<info>The [public/storage] link has been connected to [storage/uploads].</info>');
return Command::SUCCESS;
}
}
$output->writeln('<error>The links could not be created. Please check permissions.</error>');
return Command::FAILURE;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class UpCommand extends Command
{
protected static $defaultName = 'up';
protected static $defaultDescription = 'Tira a aplicação do modo de manutenção';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$downFile = dirname(__DIR__, 3) . '/storage/framework/down';
if (!file_exists($downFile)) {
$output->writeln('<comment>A aplicação já está ativa.</comment>');
return Command::SUCCESS;
}
if (unlink($downFile)) {
$output->writeln('<info>Aplicação agora está ativa.</info>');
return Command::SUCCESS;
}
$output->writeln('<error>Falha ao tirar a aplicação do modo de manutenção.</error>');
return Command::FAILURE;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputOption;
use Modules\Auth\Models\User;
class UserDeleteCommand extends Command
{
protected static $defaultName = 'user:delete';
protected static $defaultDescription = 'Remove um usuário do sistema';
protected function configure(): void
{
$this
->addArgument('user', InputArgument::REQUIRED, 'O ID ou E-mail do usuário a ser removido')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Confirmar a remoção sem prompt');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$userInput = $input->getArgument('user');
$force = $input->getOption('force');
$user = is_numeric($userInput)
? User::find($userInput)
: User::where('users_email', $userInput)->first();
if (!$user) {
$output->writeln("<error>Usuário '{$userInput}' não encontrado.</error>");
return Command::FAILURE;
}
if (!$force) {
$output->writeln("<comment>Você tem certeza que deseja remover o usuário '{$user->users_name}' ({$user->users_email})?</comment>");
$output->writeln("<info>Use a opção --force para confirmar.</info>");
return Command::FAILURE;
}
try {
$user->delete();
$output->writeln("<info>Usuário '{$user->users_name}' removido com sucesso!</info>");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao remover usuário: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Modules\Auth\Models\User;
use Symfony\Component\Console\Helper\Table;
class UserListCommand extends Command
{
protected static $defaultName = 'user:list';
protected static $defaultDescription = 'Lista todos os usuários cadastrados no sistema';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$users = User::all();
if ($users->isEmpty()) {
$output->writeln('<info>Nenhum usuário encontrado.</info>');
return Command::SUCCESS;
}
$table = new Table($output);
$table->setHeaders(['ID', 'ID Bus', 'Nome', 'Email', 'Status', 'Criado em']);
foreach ($users as $user) {
$table->addRow([
$user->users_id,
$user->users_bus_id ?? '-',
$user->users_name,
$user->users_email,
($user->users_status === 'active') ? '<info>Ativo</info>' : '<comment>' . ucfirst($user->users_status) . '</comment>',
$user->created_at
]);
}
$table->render();
$output->writeln("<info>Total de usuários: " . $users->count() . "</info>");
return Command::SUCCESS;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Modules\Auth\Models\User;
class UserPasswordCommand extends Command
{
protected static $defaultName = 'user:password';
protected static $defaultDescription = 'Altera a senha de um usuário cadastrado no sistema';
protected function configure(): void
{
$this
->addArgument('user', InputArgument::REQUIRED, 'O ID ou E-mail do usuário')
->addArgument('password', InputArgument::REQUIRED, 'A nova senha');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$userInput = $input->getArgument('user');
$newPassword = $input->getArgument('password');
$user = is_numeric($userInput)
? User::find($userInput)
: User::where('users_email', $userInput)->first();
if (!$user) {
$output->writeln("<error>Usuário '{$userInput}' não encontrado.</error>");
return Command::FAILURE;
}
try {
$user->users_password = password_hash($newPassword, PASSWORD_DEFAULT);
$user->save();
$output->writeln("<info>Senha do usuário '{$user->users_name}' alterada com sucesso!</info>");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao alterar senha: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Modules\Auth\Models\User;
class UserRegisterCommand extends Command
{
protected static $defaultName = 'user:register';
protected static $defaultDescription = 'Registra um novo usuário de forma interativa';
protected function execute(InputInterface $input, OutputInterface $output): int
{
$helper = $this->getHelper('question');
$output->writeln('<info>Cadastro de Novo Usuário</info>');
$output->writeln('-------------------------');
// Name
$nameQuestion = new Question('Nome completo: ');
$name = $helper->ask($input, $output, $nameQuestion);
if (empty($name)) {
$output->writeln('<error>O nome é obrigatório.</error>');
return Command::FAILURE;
}
// Email
$emailQuestion = new Question('E-mail: ');
$email = $helper->ask($input, $output, $emailQuestion);
if (empty($email) || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
$output->writeln('<error>E-mail inválido ou vazio.</error>');
return Command::FAILURE;
}
if (User::where('users_email', $email)->exists()) {
$output->writeln("<error>Já existe um usuário com o e-mail {$email}.</error>");
return Command::FAILURE;
}
// Password
$passwordQuestion = new Question('Senha: ');
$passwordQuestion->setHidden(true);
$passwordQuestion->setHiddenFallback(false);
$password = $helper->ask($input, $output, $passwordQuestion);
if (strlen($password) < 6) {
$output->writeln('<error>A senha deve ter pelo menos 6 caracteres.</error>');
return Command::FAILURE;
}
// Bus ID
$busIdQuestion = new Question('ID Bus (opcional): ');
$busId = $helper->ask($input, $output, $busIdQuestion);
try {
$user = new User();
$user->users_name = $name;
$user->users_email = $email;
$user->users_password = password_hash($password, PASSWORD_DEFAULT);
$user->users_role = 'user';
$user->users_bus_id = $busId;
$user->users_status = 'active';
$user->save();
$output->writeln("\n<info>Usuário '{$name}' cadastrado com sucesso!</info>");
$output->writeln("ID: {$user->users_id}");
$output->writeln("E-mail: {$user->users_email}");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao cadastrar usuário: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+51
View File
@@ -0,0 +1,51 @@
<?php
namespace App\Console\Commands;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Modules\Auth\Models\User;
class UserRoleCommand extends Command
{
protected static $defaultName = 'user:role';
protected static $defaultDescription = 'Altera o nível de acesso (role) de um usuário';
protected function configure(): void
{
$this
->addArgument('user', InputArgument::REQUIRED, 'O ID ou E-mail do usuário')
->addArgument('role', InputArgument::REQUIRED, 'O novo nível de acesso (ex: user, admin, super_admin)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$userInput = $input->getArgument('user');
$newRole = strtolower($input->getArgument('role'));
$user = is_numeric($userInput)
? User::find($userInput)
: User::where('users_email', $userInput)->first();
if (!$user) {
$output->writeln("<error>Usuário '{$userInput}' não encontrado.</error>");
return Command::FAILURE;
}
try {
$oldRole = $user->users_role;
$user->users_role = $newRole;
$user->save();
$output->writeln("<info>Nível de acesso do usuário '{$user->users_name}' alterado com sucesso!</info>");
$output->writeln("De: <comment>{$oldRole}</comment> para: <info>{$newRole}</info>");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Erro ao alterar nível de acesso: " . $e->getMessage() . "</error>");
return Command::FAILURE;
}
}
}
+653
View File
@@ -0,0 +1,653 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Controllers;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Modules\Auth\Models\User;
use App\Models\Module;
use Modules\Company\Models\Company;
use Illuminate\Database\Capsule\Manager as DB;
class AdminController extends BaseAdminController
{
/**
* Display the main dashboard.
*
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user) {
return new Response('', 302, ['Location' => url('/login')]);
}
// Get enabled modules for the user's company
$enabledModules = $this->getEnabledModules($user);
// Get widgets from authorized modules
$widgets = $this->getAuthorizedWidgets($enabledModules, $user);
// Get system statistics
$stats = $this->getSystemStats($user);
// Sidebar and common data are handled by renderAdminView,
// but since dashboard/index.twig expects specific variables,
// we can still pass them explicitly if we use the old way,
// OR we can use renderAdminView and just pass the extra data.
// Check if SEO module is installed but not configured
$seoPending = false;
try {
$seoModule = Module::where('modules_name', 'seo')->active()->first();
if ($seoModule && empty($seoModule->modules_settings)) {
$seoPending = true;
}
} catch (\Throwable $e) {
// Silently fail if something goes wrong with SEO check
error_log("SEO Check Error: " . $e->getMessage());
}
// Using renderAdminView for consistency:
return $this->renderAdminView('dashboard/index.twig', [
'stats' => $stats,
'recent_activity' => $this->getRecentActivity($user),
'widgets' => $widgets,
'seo_pending' => $seoPending,
'cache_cleared' => $request->query->get('cache_cleared'),
'deleted_count' => $request->query->get('deleted'),
'opcache_cleared' => $request->query->get('opcache')
]);
}
/**
* Get authorized widgets for the user.
*
* @param \Illuminate\Database\Eloquent\Collection $modules
* @param User $user
* @return array
*/
protected function getAuthorizedWidgets($modules, User $user): array
{
$widgets = [];
foreach ($modules as $module) {
if (!$this->userHasModulePermission($user, $module)) {
continue;
}
$moduleNamespace = $module->modules_namespace;
// If namespace is not defined, try to guess it from name (fallback)
if (!$moduleNamespace) {
$moduleNamespace = "Modules\\" . ucfirst($module->modules_name);
}
// Ensure namespace starts with \Modules\
if (strpos($moduleNamespace, 'Modules\\') !== 0 && strpos($moduleNamespace, '\\Modules\\') !== 0) {
$moduleNamespace = "Modules\\" . ltrim($moduleNamespace, '\\');
}
$widgetClass = (strpos($moduleNamespace, '\\') === 0 ? "" : "\\") . $moduleNamespace . "\\Widgets\\DashboardWidget";
if (class_exists($widgetClass)) {
try {
$widget = new $widgetClass();
// Check if user can view this widget
if ($this->userCanViewWidget($user, $widget)) {
$widgets[] = $widget->render();
}
} catch (\Throwable $e) {
error_log("Error loading widget {$widgetClass}: " . $e->getMessage());
}
}
}
return $widgets;
}
/**
* Check if user can view a widget.
*
* @param User $user
* @param mixed $widget
* @return bool
*/
protected function userCanViewWidget(User $user, $widget): bool
{
// Check if widget defines required permissions
if (method_exists($widget, 'getRequiredPermission')) {
$requiredPermission = $widget->getRequiredPermission();
return $user->hasPermission($requiredPermission);
}
// If no specific permission, check module access
return true;
}
/**
* Get system statistics.
*
* @param User $user
* @return array
*/
protected function getSystemStats(User $user): array
{
try {
if ($user->isSuperAdmin()) {
return $this->getSuperAdminStats();
}
return $this->getCompanyStats($user);
} catch (\Exception $e) {
error_log("Error loading system stats: " . $e->getMessage());
return [
'total_companies' => 0,
'active_companies' => 0,
'total_users' => 0,
'active_users' => 0,
'total_modules' => 0,
'active_modules' => 0,
'user_count' => 0,
'enabled_modules' => 0
];
}
}
/**
* Get statistics for Super Admin.
*
* @return array
*/
protected function getSuperAdminStats(): array
{
return [
'total_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::count() : 0,
'active_companies' => class_exists('Modules\Company\Models\Company') ? \Modules\Company\Models\Company::active()->count() : 0,
'total_users' => User::count(),
'active_users' => User::active()->count(),
'total_modules' => Module::count(),
'active_modules' => Module::active()->count()
];
}
/**
* Get statistics for company users.
*
* @param User $user
* @return array
*/
protected function getCompanyStats(User $user): array
{
$companyId = $user->users_bus_id;
return [
'user_count' => User::where('users_bus_id', $companyId)->count(),
'active_users' => User::where('users_bus_id', $companyId)->active()->count(),
'enabled_modules' => DB::table('bus_modules')
->where('bus_modules_bus_id', $companyId)
->where('bus_modules_is_enabled', true)
->count()
];
}
/**
* Get recent activity for the user.
*
* @param User $user
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getRecentActivity(User $user)
{
try {
if (!class_exists('App\Models\ActivityLog')) {
return collect([]);
}
return \App\Models\ActivityLog::with('user')
->forUser($user->users_id)
->orderByDate('desc')
->limit(10)
->get();
} catch (\Exception $e) {
error_log("Error loading recent activity: " . $e->getMessage());
return collect([]);
}
}
/**
* Show module installation form.
*
* @param Request $request
* @return Response
*/
public function showInstallModule(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response('', 302, ['Location' => url('/dashboard')]);
}
$success = isset($_SESSION['flash_success']) ? (array) $_SESSION['flash_success'] : [];
$error = isset($_SESSION['flash_error']) ? (array) $_SESSION['flash_error'] : [];
unset($_SESSION['flash_success'], $_SESSION['flash_error']);
return $this->renderAdminView('modules/install.twig', [
'error' => $error,
'success' => $success
]);
}
/**
* Install a module from zip file.
*
* @param Request $request
* @return Response
*/
public function installModule(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response('', 302, ['Location' => url('/dashboard')]);
}
try {
// Validate uploaded file
if (!$request->files->has('module_zip')) {
throw new \Exception(__('modules.error_no_file'));
}
$file = $request->files->get('module_zip');
// Validate file type
if ($file->getMimeType() !== 'application/zip' && $file->getMimeType() !== 'application/x-zip-compressed') {
throw new \Exception(__('modules.error_invalid_zip'));
}
// Create temporary directory
$tempDir = sys_get_temp_dir() . '/module_install_' . time();
mkdir($tempDir, 0755, true);
// Extract zip file
$zip = new \ZipArchive();
if ($zip->open($file->getPathname()) !== TRUE) {
throw new \Exception(__('modules.error_open_zip'));
}
if (!$zip->extractTo($tempDir)) {
throw new \Exception(__('modules.error_extract_zip'));
}
$zip->close();
// Validate module structure
$this->validateModuleStructure($tempDir);
// Read module configuration
$moduleConfig = $this->readModuleConfig($tempDir);
// Check if module already exists
if (Module::where('modules_name', $moduleConfig['name'])->exists()) {
throw new \Exception(__('modules.error_module_exists'));
}
// Copy module files to modules directory
$modulesPath = dirname(__DIR__, 2) . '/modules/' . $moduleConfig['name'];
$this->recursiveCopy($tempDir, $modulesPath);
// Run migrations if they exist
$this->runModuleMigrations($modulesPath);
// Check if module was registered by migrations
$module = Module::where('modules_name', $moduleConfig['name'])->first();
if (!$module) {
// Register module in database
$module = $this->registerModule($moduleConfig);
}
// Enable module for all companies if specified
if ($request->request->get('enable_for_all', false)) {
$this->enableModuleForAllCompanies($module->modules_id, $user->users_id);
}
// Clean up
$this->recursiveDelete($tempDir);
$_SESSION['flash_success'][] = __('modules.success_install');
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
} catch (\Throwable $e) {
// Clean up on error
if (isset($tempDir) && is_dir($tempDir)) {
$this->recursiveDelete($tempDir);
}
$_SESSION['flash_error'][] = $e->getMessage();
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
}
}
/**
* Validate module structure.
*
* @param string $modulePath
* @throws \Exception
*/
protected function validateModuleStructure(string $modulePath): void
{
$requiredFiles = ['module.json'];
foreach ($requiredFiles as $file) {
if (!file_exists($modulePath . '/' . $file)) {
throw new \Exception(__('modules.error_missing_file', ['file' => $file]));
}
}
}
/**
* Read module configuration.
*
* @param string $modulePath
* @return array
* @throws \Exception
*/
protected function readModuleConfig(string $modulePath): array
{
$configPath = $modulePath . '/module.json';
if (!file_exists($configPath)) {
throw new \Exception(__('modules.error_missing_config'));
}
$config = json_decode(file_get_contents($configPath), true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception(__('modules.error_invalid_config', ['error' => json_last_error_msg()]));
}
$requiredFields = ['name', 'version', 'description'];
foreach ($requiredFields as $field) {
if (empty($config[$field])) {
throw new \Exception(__('modules.error_missing_field', ['field' => $field]));
}
}
return $config;
}
/**
* Recursively copy directory.
*
* @param string $source
* @param string $destination
*/
protected function recursiveCopy(string $source, string $destination): void
{
if (!is_dir($destination)) {
mkdir($destination, 0755, true);
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $item) {
// Calculate subPathName manually to ensure compatibility
$len = strlen($source);
$subPathName = substr($item->getPathname(), $len + 1);
$targetPath = $destination . '/' . $subPathName;
if ($item->isDir()) {
mkdir($targetPath, 0755, true);
} else {
copy($item, $targetPath);
}
}
}
/**
* Recursively delete directory.
*
* @param string $dir
*/
protected function recursiveDelete(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->recursiveDelete($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
/**
* Run module migrations.
*
* @param string $modulePath
*/
protected function runModuleMigrations(string $modulePath): void
{
$migrationsPath = $modulePath . '/database/migrations';
if (!is_dir($migrationsPath)) {
return;
}
$migrations = glob($migrationsPath . '/*.php');
foreach ($migrations as $migration) {
require_once $migration;
// Try to find class name from file content
$content = file_get_contents($migration);
if (preg_match('/class\s+(\w+)/', $content, $matches)) {
$className = $matches[1];
if (class_exists($className)) {
$instance = new $className();
if (method_exists($instance, 'up')) {
try {
$instance->up();
} catch (\Exception $e) {
error_log("Migration failed for $className: " . $e->getMessage());
throw $e; // Re-throw to show error to user
}
}
} else {
error_log("Class $className not found in migration file: $migration");
}
} else {
error_log("Could not find class name in migration file: $migration");
}
}
}
/**
* Fix module menus in database.
*
* @return Response
*/
public function fixModuleMenus(): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response('Access denied', 403);
}
$modules = Module::all();
$count = 0;
foreach ($modules as $module) {
$modulePath = dirname(__DIR__, 2) . '/' . $module->modules_path;
try {
$config = $this->readModuleConfig($modulePath);
// Update module settings with menu
$module->modules_settings = array_merge($module->modules_settings ?? [], $config['settings'] ?? [], ['menu' => $config['menu'] ?? []]);
// Also update top-level module fields that might be in module.json
if (isset($config['display_name'])) $module->modules_display_name = $config['display_name'];
if (isset($config['description'])) $module->modules_description = $config['description'];
if (isset($config['version'])) $module->modules_version = $config['version'];
if (isset($config['icon'])) $module->modules_icon = $config['icon'];
if (isset($config['menu_side_bar'])) $module->menu_side_bar = (bool)$config['menu_side_bar'];
if (isset($config['menu_nav_bar'])) $module->menu_nav_bar = (bool)$config['menu_nav_bar'];
if (isset($config['order'])) $module->modules_order = (int)$config['order'];
if (isset($config['namespace'])) $module->modules_namespace = $config['namespace'];
$module->save();
$count++;
} catch (\Exception $e) {
// Ignore errors for individual modules
error_log("Failed to update menu for module {$module->modules_name}: " . $e->getMessage());
}
}
$_SESSION['flash_success'][] = __('modules.success_menus_updated', ['count' => $count]);
return new Response('', 302, ['Location' => url('/admin/modules/install')]);
}
/**
* Register module in database.
*
* @param array $config
* @return Module
*/
protected function registerModule(array $config): Module
{
return Module::create([
'modules_name' => $config['name'],
'modules_display_name' => $config['display_name'] ?? $config['name'],
'modules_description' => $config['description'],
'modules_version' => $config['version'],
'modules_author' => $config['author'] ?? 'Unknown',
'modules_ative' => true,
'modules_public' => $config['public'] ?? false,
'modules_blocked' => false,
'modules_dependencies' => $config['dependencies'] ?? [],
'modules_permissions' => $config['permissions'] ?? [],
'modules_settings' => array_merge($config['settings'] ?? [], ['menu' => $config['menu'] ?? []]),
'modules_path' => 'modules/' . $config['name'],
'modules_namespace' => $config['namespace'] ?? ('Modules\\' . ucfirst($config['name'])),
'modules_icon' => $config['icon'] ?? 'bi bi-gear',
'menu_side_bar' => $config['menu_side_bar'] ?? true,
'menu_nav_bar' => $config['menu_nav_bar'] ?? false,
'modules_order' => $config['order'] ?? 999,
'modules_install_date' => \Carbon\Carbon::now(),
'modules_update_date' => \Carbon\Carbon::now()
]);
}
/**
* Enable module for all companies.
*
* @param int $moduleId
* @param int $enabledBy
*/
protected function enableModuleForAllCompanies(int $moduleId, int $enabledBy): void
{
$companies = Company::all();
foreach ($companies as $company) {
$company->enableModule($moduleId, $enabledBy);
}
}
/**
* Search for modules and users.
*
* @param Request $request
* @return Response
*/
public function search(Request $request): Response
{
$user = $this->getCurrentUser();
$query = $request->query->get('q');
if (!$user) {
return new Response('', 302, ['Location' => url('/login')]);
}
if (empty($query)) {
return $this->renderAdminView('dashboard/search_results.twig', [
'query' => '',
'results' => ['modules' => [], 'users' => []]
]);
}
$results = [
'modules' => [],
'users' => []
];
// Search Modules
$results['modules'] = Module::where('modules_name', 'LIKE', "%{$query}%")
->orWhere('modules_display_name', 'LIKE', "%{$query}%")
->orWhere('modules_description', 'LIKE', "%{$query}%")
->get();
// Search Users (Super Admin or Company Admin)
if ($user->isSuperAdmin()) {
$results['users'] = User::where('users_name', 'LIKE', "%{$query}%")
->orWhere('users_email', 'LIKE', "%{$query}%")
->get();
} elseif ($user->users_bus_id) {
$results['users'] = User::where('users_bus_id', $user->users_bus_id)
->where(function($q) use ($query) {
$q->where('users_name', 'LIKE', "%{$query}%")
->orWhere('users_email', 'LIKE', "%{$query}%");
})
->get();
}
return $this->renderAdminView('dashboard/search_results.twig', [
'query' => $query,
'results' => $results
]);
}
}
+465
View File
@@ -0,0 +1,465 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Modules\Auth\Models\User;
use App\Models\Module;
use Modules\Company\Models\Company;
use Illuminate\Database\Capsule\Manager as DB;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
abstract class BaseAdminController
{
protected View $view;
public function __construct(View $view)
{
$this->view = $view;
}
/**
* Render an admin view with common data (sidebar, user, etc).
*
* @param string $viewName
* @param array $data
* @return Response
*/
protected function renderAdminView(string $viewName, array $data = []): Response
{
$user = $this->getCurrentUser();
if (!$user) {
return new Response('', 302, ['Location' => url('/login')]);
}
// Get flash messages
$flashMessages = [];
if (isset($_SESSION['flash'])) {
$flashMessages = $_SESSION['flash'];
unset($_SESSION['flash']);
}
// Get enabled modules for the user's company
$enabledModules = $this->getEnabledModules($user);
// Build sidebar based on user permissions
$sidebarItems = $this->buildUserSidebar($enabledModules, $user);
// Build navbar based on user permissions
$navbarItems = $this->buildUserNavbar($enabledModules, $user);
// Get admin theme slug (default sb-admin)
$adminThemeSlug = 'sb-admin';
// Get config pages visibility
$configPagesVisibility = [];
if (class_exists('Modules\Configindex\Models\ConfigPage')) {
$baseDir = dirname(dirname(__DIR__));
$activeTheme = \Kernel\View::getActiveTheme() ?? 'default';
$indexViewsDir = $baseDir . '/modules/index/views';
$themeViewsDir = $baseDir . '/themes/' . $activeTheme . '/modules/index/views';
$pages = \Modules\Configindex\Models\ConfigPage::where('config_index_bus_id', $user->users_bus_id)->get();
foreach ($pages as $page) {
$key = $page->config_index_key;
// Verifica se o arquivo físico existe
$fileExists = false;
if ($key === 'index') {
$fileExists = true;
} else {
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
$baseDir . '/modules/' . $key . '/views/index.twig',
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
}
$configPagesVisibility[$key] = [
'public' => (bool)$page->config_index_show_public_nav && $fileExists,
'dashboard' => (bool)$page->config_index_show_dashboard_nav && $fileExists,
'title' => $page->config_index_title ?: ucfirst($key)
];
}
// Garante que as páginas padrão tenham uma configuração inicial, mas respeita o banco de dados
foreach (['about', 'contact', 'services'] as $key) {
if (!isset($configPagesVisibility[$key])) {
$configPagesVisibility[$key] = ['public' => false, 'dashboard' => false, 'title' => ucfirst($key)];
}
}
}
$commonData = [
'auth' => ['user' => $user],
'user' => $user,
'sidebar_items' => $sidebarItems,
'navbar_items' => $navbarItems,
'current_company' => $this->getCurrentCompany($user),
'admin_theme_slug' => $adminThemeSlug,
'config_pages_visibility' => $configPagesVisibility,
'flash_messages' => $flashMessages
];
return new Response($this->view->render($viewName, array_merge($commonData, $data)));
}
protected function redirect(string $path, int $status = 302): Response
{
return new RedirectResponse(url($path), $status);
}
/**
* Get current authenticated user.
*
* @return User|null
*/
protected function getCurrentUser(): ?User
{
if (!isset($_SESSION['user_id'])) {
return null;
}
try {
return User::find($_SESSION['user_id']);
} catch (\Exception $e) {
error_log("Error loading user: " . $e->getMessage());
return null;
}
}
/**
* Check if current user can edit/delete an object.
* Only author, admin or super_admin are allowed.
*
* @param int|string $authorId
* @return bool
*/
protected function canEditObject($authorId): bool
{
$user = $this->getCurrentUser();
if (!$user) {
return false;
}
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
return (int) $user->users_id === (int) $authorId;
}
/**
* Get enabled modules for the user.
*
* @param User $user
* @return \Illuminate\Database\Eloquent\Collection
*/
protected function getEnabledModules(User $user)
{
if ($user->isSuperAdmin()) {
// Super Admin sees all active modules
return Module::where('modules_ative', true)
->orderBy('modules_order', 'asc')
->get();
}
// For other users, get modules enabled for their company
$modules = Module::join('bus_modules', 'modules.modules_id', '=', 'bus_modules.bus_modules_module_id')
->where('bus_modules.bus_modules_bus_id', $user->users_bus_id)
->where('bus_modules.bus_modules_is_enabled', true)
->where('modules.modules_ative', true)
->orderBy('modules.modules_order', 'asc')
->select('modules.*', 'bus_modules.bus_modules_show_public_nav', 'bus_modules.bus_modules_show_dashboard_nav')
->get();
// If user is Admin, they see everything enabled for the company.
// If user is Member or User, we MUST ensure they see modules they have explicit permissions for,
// even if the module doesn't have bus_modules_show_dashboard_nav = true (if that was the blocker).
// If we're missing core modules (auth, index) in the company's enabled list,
// we should add them manually if they are active system-wide,
// because they are essential for the dashboard to function.
// We also add 'company' to ensure Admins can always manage their company details.
$coreModules = Module::whereIn('modules_name', ['auth', 'index', 'company'])
->where('modules_ative', true)
->get();
foreach ($coreModules as $core) {
if (!$modules->contains('modules_name', $core->modules_name)) {
$modules->push($core);
}
}
return $modules;
}
/**
* Build user sidebar based on permissions.
*
* @param \Illuminate\Database\Eloquent\Collection $modules
* @param User $user
* @return array
*/
protected function buildUserSidebar($modules, User $user): array
{
$sidebarItems = [];
// Ensure company settings link is always present for Admins
if ($user->isAdmin() || $user->isSuperAdmin()) {
$sidebarItems[] = [
'icon' => 'bi bi-building',
'label' => __('company.menu.dashboard'),
'route' => 'admin/company/settings',
'order' => 30
];
}
foreach ($modules as $module) {
// Skip company module if we already added it manually, or auth/index
if ($module->modules_name === 'auth' || $module->modules_name === 'index' || $module->modules_name === 'company') {
continue;
}
// Check if module is enabled for sidebar
if (!$module->menu_side_bar) {
continue;
}
$moduleConfig = $module->modules_settings ?: [];
// If settings are empty, try to load from module.json file as a fallback
if (empty($moduleConfig) || !isset($moduleConfig['menu'])) {
$moduleJsonPath = dirname(__DIR__, 2) . '/modules/' . $module->modules_name . '/module.json';
if (file_exists($moduleJsonPath)) {
$jsonContent = json_decode(file_get_contents($moduleJsonPath), true);
if ($jsonContent && isset($jsonContent['menu'])) {
$moduleConfig['menu'] = $jsonContent['menu'];
}
}
}
// Fix for company module if settings are still empty (due to migration issue)
if ((empty($moduleConfig) || !isset($moduleConfig['menu'])) && $module->modules_name === 'company') {
$moduleConfig['menu'] = [
'dashboard' => [
'icon' => 'bi bi-building',
'label' => 'company.menu.dashboard',
'route' => 'admin/company/settings',
'order' => 30
]
];
}
if (isset($moduleConfig['menu']['dashboard'])) {
$menuItem = $moduleConfig['menu']['dashboard'];
// Check if user can access this menu item
// Respect company visibility preference when not super admin or admin
// $companyAllows = ($user->isSuperAdmin() || $user->isAdmin()) || (isset($module->bus_modules_show_dashboard_nav) ? (bool) $module->bus_modules_show_dashboard_nav : true);
// Simplified logic: if it's in the $modules list (which already filters by company enablement)
// we only need to check the user's specific permissions.
if ($this->userCanAccessMenuItem($user, $menuItem, $module)) {
$sidebarItems[] = [
'icon' => $menuItem['icon'] ?? $module->modules_icon,
'label' => __($menuItem['label'] ?? $module->modules_display_name),
'route' => $menuItem['route'] ?? 'admin/' . $module->modules_name,
'order' => $menuItem['order'] ?? $module->modules_order
];
}
}
}
// Sort by order
usort($sidebarItems, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $sidebarItems;
}
/**
* Build user navbar based on permissions.
*
* @param \Illuminate\Database\Eloquent\Collection $modules
* @param User $user
* @return array
*/
protected function buildUserNavbar($modules, User $user): array
{
$navbarItems = [];
foreach ($modules as $module) {
// Check if module is enabled for navbar
if (!$module->menu_nav_bar) {
continue;
}
$moduleConfig = $module->modules_settings ?: [];
// If settings are empty, try to load from module.json file as a fallback
if (empty($moduleConfig) || !isset($moduleConfig['menu'])) {
$moduleJsonPath = dirname(__DIR__, 2) . '/modules/' . $module->modules_name . '/module.json';
if (file_exists($moduleJsonPath)) {
$jsonContent = json_decode(file_get_contents($moduleJsonPath), true);
if ($jsonContent && isset($jsonContent['menu'])) {
$moduleConfig['menu'] = $jsonContent['menu'];
}
}
}
// Try to get specific navbar config, otherwise fallback to public or dashboard/general
if (isset($moduleConfig['menu']['navbar'])) {
$menuItem = $moduleConfig['menu']['navbar'];
} elseif (isset($moduleConfig['menu']['public'])) {
$menuItem = $moduleConfig['menu']['public'];
} elseif (isset($moduleConfig['menu']['dashboard'])) {
$menuItem = $moduleConfig['menu']['dashboard'];
} else {
continue;
}
// Check if user can access this menu item
// Respect company visibility preference when not super admin or admin
// $companyAllows = ($user->isSuperAdmin() || $user->isAdmin()) || (isset($module->bus_modules_show_public_nav) ? (bool) $module->bus_modules_show_public_nav : true);
if ($this->userCanAccessMenuItem($user, $menuItem, $module)) {
$route = $menuItem['route'] ?? $module->modules_name;
$navbarItems[] = [
'icon' => $menuItem['icon'] ?? $module->modules_icon,
'label' => __($menuItem['label'] ?? $module->modules_display_name),
'route' => $route,
'order' => $menuItem['order'] ?? $module->modules_order
];
}
}
// Sort by order
usort($navbarItems, function ($a, $b) {
return $a['order'] <=> $b['order'];
});
return $navbarItems;
}
/**
* Check if user can access a menu item.
*
* @param User $user
* @param array $menuItem
* @param Module|null $module
* @return bool
*/
protected function userCanAccessMenuItem(User $user, array $menuItem, ?Module $module = null): bool
{
// Custom restriction for companies management (Must check before general admin/superadmin bypass)
if (isset($menuItem['route']) && $menuItem['route'] === 'admin/companies') {
return $user->isSuperAdmin();
}
// Super Admin and Admin have access to everything authorized for the company
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
// If specific permission defined for menu item, check it
if (!empty($menuItem['permission'])) {
return $user->hasPermission($menuItem['permission']);
}
// If no specific permission, but a module is provided
if ($module) {
// If the module is a core/blocked module, it's generally accessible
// (like Auth or Index)
if ($module->isBlocked()) {
return true;
}
// For Member role, if no specific permission is required for the menu item,
// we check if they have ANY permission for this module.
// This ensures that if an Admin gave them "pagebuilder.view", the Page Builder
// module link appears in the sidebar.
return $this->userHasModulePermission($user, $module);
}
// Default to allowed for items without permissions or module context
return true;
}
/**
* Get current company for the user.
*
* @param User $user
* @return Company|null
*/
protected function getCurrentCompany(User $user): ?Company
{
if ($user->isSuperAdmin()) {
return null; // Super Admin can switch companies
}
return Company::find($user->users_bus_id);
}
/**
* Check if user has permission for module.
*
* @param User $user
* @param Module $module
* @return bool
*/
protected function userHasModulePermission(User $user, Module $module): bool
{
// Super Admin and Admin have access to all modules enabled for their company
if ($user->isSuperAdmin() || $user->isAdmin()) {
return true;
}
// Core modules are accessible to all authenticated users
if ($module->isBlocked()) {
return true;
}
$modulePermissions = $module->getPermissionsAttribute();
// If module has no permissions defined and is public, it might be a general utility
if (empty($modulePermissions) && $module->isPublic()) {
return true;
}
foreach ($modulePermissions as $permission) {
if ($user->hasPermission($permission)) {
return true;
}
}
return false;
}
/**
* Add a flash message to the session.
*
* @param string $type success|error|info|warning
* @param string $message
*/
protected function addFlash(string $type, string $message): void
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$_SESSION['flash'][$type] = $message;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Response;
abstract class BaseController
{
protected View $view;
public function __construct(View $view)
{
$this->view = $view;
}
/**
* Render a view with data.
*
* @param string $viewName
* @param array $data
* @return Response
*/
protected function renderView(string $viewName, array $data = []): Response
{
// Get config pages visibility
$configPagesVisibility = [];
if (class_exists('Modules\Configindex\Models\ConfigPage')) {
$busId = $_SESSION['user_idbus'] ?? $_SESSION['current_company_id'] ?? 1;
$baseDir = dirname(dirname(__DIR__));
$activeTheme = \Kernel\View::getActiveTheme() ?? 'default';
$indexViewsDir = $baseDir . '/modules/index/views';
$themeViewsDir = $baseDir . '/themes/' . $activeTheme . '/modules/index/views';
$pages = \Modules\Configindex\Models\ConfigPage::where('config_index_bus_id', $busId)->get();
foreach ($pages as $page) {
$key = $page->config_index_key;
// Verifica se o arquivo físico existe
$fileExists = false;
if ($key === 'index') {
$fileExists = true;
} else {
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
$baseDir . '/modules/' . $key . '/views/index.twig', // Adicionado para checar módulos
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
}
$configPagesVisibility[$key] = [
'public' => (bool)$page->config_index_show_public_nav && $fileExists,
'dashboard' => (bool)$page->config_index_show_dashboard_nav && $fileExists,
'title' => $page->config_index_title ?: ucfirst($key)
];
}
// Garante que as páginas padrão tenham uma configuração inicial, mas respeita o banco de dados
foreach (['about', 'contact', 'services'] as $key) {
if (!isset($configPagesVisibility[$key])) {
// Verifica se o arquivo físico existe para os padrões também
$fileExists = false;
$possiblePaths = [
$indexViewsDir . '/' . $key . '.twig',
$indexViewsDir . '/' . $key . '.php',
$themeViewsDir . '/' . $key . '.twig',
$themeViewsDir . '/' . $key . '.php',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.twig',
$baseDir . '/themes/' . $activeTheme . '/' . $key . '.php',
];
foreach ($possiblePaths as $path) {
if (is_file($path)) {
$fileExists = true;
break;
}
}
// Se não estiver no banco, o padrão é oculto (false) para evitar exibição indesejada
$configPagesVisibility[$key] = ['public' => false, 'dashboard' => false, 'title' => ucfirst($key)];
}
}
}
// Get SEO settings
$seoSettings = [];
if (class_exists('App\Models\Module')) {
$seoModule = \App\Models\Module::where('modules_name', 'seo')->first();
if ($seoModule && $seoModule->modules_settings) {
$seoSettings = $seoModule->modules_settings;
}
}
$commonData = [
'config_pages_visibility' => $configPagesVisibility,
'seo' => $seoSettings
];
return new Response($this->view->render($viewName, array_merge($commonData, $data)));
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class QuillDemoController extends BaseAdminController
{
/**
* Displays the Quill demo page
*/
public function index(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Render the demo view
$view = new View();
$content = $view->render('examples/quill-demo.twig');
return new Response($content);
}
/**
* Processes the demo form submission
*/
public function submit(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Get form data
$title = $request->get('title');
$content = $request->get('content');
$advancedContent = $request->get('advanced_content');
$simpleContent = $request->get('simple_content');
// Save to session to display on results page
$_SESSION['form_data'] = [
'title' => $title,
'content' => $content,
'advanced_content' => $advancedContent,
'simple_content' => $simpleContent
];
// Redirect to results page
return $this->redirectToRoute('quill-demo-results');
}
/**
* Displays form results
*/
public function results(Request $request): Response
{
// Check if the user is authenticated
if (!$this->isAuthenticated()) {
return $this->redirectToRoute('login');
}
// Get session data
$formData = $_SESSION['form_data'] ?? [];
// Clear session data
unset($_SESSION['form_data']);
// Render the results view
$view = new View();
$content = $view->render('examples/quill-results.twig', [
'formData' => $formData
]);
return new Response($content);
}
/**
* Checks if the user is authenticated
*/
private function isAuthenticated(): bool
{
return isset($_SESSION['user_id']);
}
/**
* Redirects to a route
*/
private function redirectToRoute(string $route): Response
{
$url = url($route);
return new Response("", 302, ["Location" => $url]);
}
}
+98
View File
@@ -0,0 +1,98 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
class StorageController extends BaseController
{
/**
* Serve a file from storage/uploads/{module}/{filename}
*
* @param Request $request
* @param string $module
* @param string $filename
* @return Response
*/
public function serve(Request $request, string $module, string $filename): Response
{
// Sanitize inputs to prevent directory traversal
$module = preg_replace('/[^a-zA-Z0-9_-]/', '', $module);
// Allow subfolders in filename (e.g. "123/image.jpg")
$filename = ltrim($filename, '/');
if (strpos($filename, '..') !== false) {
return new Response('Access denied.', 403);
}
// Build the physical path
$basePath = dirname(__DIR__, 2) . '/storage/uploads';
$filePath = $basePath . '/' . $module . '/' . $filename;
// Security check: Ensure the file is actually inside storage/uploads
$realPath = realpath($filePath);
if (!$realPath || strpos($realPath, realpath($basePath)) !== 0) {
return new Response('File not found or access denied.', 404);
}
if (!file_exists($realPath)) {
return new Response('File not found.', 404);
}
// Determine MIME type
$mimeType = $this->getMimeType($realPath);
// Create BinaryFileResponse for efficient file serving
$response = new BinaryFileResponse($realPath);
$response->headers->set('Content-Type', $mimeType);
$response->setContentDisposition(
ResponseHeaderBag::DISPOSITION_INLINE,
$filename
);
// Add cache headers (cache for 30 days)
$response->setPublic();
$response->setMaxAge(2592000);
$response->setSharedMaxAge(2592000);
return $response;
}
/**
* Get MIME type of a file
*/
private function getMimeType(string $path): string
{
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
$mimes = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
'webp' => 'image/webp',
'svg' => 'image/svg+xml',
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'txt' => 'text/plain',
'zip' => 'application/zip',
];
if (isset($mimes[$extension])) {
return $mimes[$extension];
}
if (function_exists('mime_content_type')) {
return mime_content_type($path);
}
return 'application/octet-stream';
}
}
+302
View File
@@ -0,0 +1,302 @@
<?php
namespace App\Controllers;
use Kernel\View;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Modules\Auth\Models\User;
use Illuminate\Database\Capsule\Manager as DB;
class SystemController extends BaseAdminController
{
private $logPath;
private $cachePath;
public function __construct(View $view)
{
parent::__construct($view);
$this->logPath = dirname(__DIR__, 2) . '/logs';
$this->cachePath = dirname(__DIR__, 2) . '/cache';
}
/**
* Display system information.
*
* @param Request $request
* @return Response
*/
public function info(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$dbVersion = 'Unknown';
try {
$pdo = DB::connection()->getPdo();
$dbVersion = $pdo->getAttribute(\PDO::ATTR_SERVER_VERSION);
} catch (\Exception $e) {
$dbVersion = 'Error: ' . $e->getMessage();
}
$info = [
'app' => [
'name' => 'SysCMS',
'version' => '1.0.0', // Should be from config or const
'env' => getenv('APP_ENV') ?: 'production',
'debug' => getenv('APP_DEBUG') ?: false,
],
'system' => [
'os' => php_uname('s') . ' ' . php_uname('r'),
'hostname' => gethostname(),
'server_software' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
'timezone' => date_default_timezone_get(),
],
'php' => [
'version' => phpversion(),
'sapi' => php_sapi_name(),
'memory_limit' => ini_get('memory_limit'),
'max_execution_time' => ini_get('max_execution_time') . 's',
'upload_max_filesize' => ini_get('upload_max_filesize'),
'post_max_size' => ini_get('post_max_size'),
'extensions' => get_loaded_extensions(),
],
'database' => [
'driver' => DB::connection()->getDriverName(),
'version' => $dbVersion,
'database' => DB::connection()->getDatabaseName(),
],
'disk' => [
'total' => $this->formatBytes(disk_total_space('.')),
'free' => $this->formatBytes(disk_free_space('.')),
'used' => $this->formatBytes(disk_total_space('.') - disk_free_space('.')),
]
];
return $this->renderAdminView('system/info.twig', [
'page_title' => 'System Information',
'info' => $info
]);
}
/**
* Display PHP Info.
*
* @param Request $request
* @return Response
*/
public function phpInfo(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response('Access denied', 403);
}
ob_start();
phpinfo();
$phpinfo = ob_get_clean();
// Extract body content to embed cleanly if desired, or just output raw.
// For admin panel consistency, usually raw in a new tab is easiest,
// but embedding provides better UX.
// Let's strip body tags to embed.
$phpinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $phpinfo);
return $this->renderAdminView('system/phpinfo.twig', [
'page_title' => 'PHP Info',
'phpinfo' => $phpinfo
]);
}
/**
* List all log files.
*
* @param Request $request
* @return Response
*/
public function logsIndex(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$files = [];
if (is_dir($this->logPath)) {
$files = glob($this->logPath . '/*.log');
}
// Sort by modification time desc
usort($files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$logs = [];
foreach ($files as $file) {
$logs[] = [
'name' => basename($file),
'size' => $this->formatBytes(filesize($file)),
'modified' => date('Y-m-d H:i:s', filemtime($file))
];
}
return $this->renderAdminView('logs/index.twig', [
'page_title' => 'System Logs',
'logs' => $logs
]);
}
/**
* Show log file content.
*
* @param Request $request
* @return Response
*/
public function logsShow(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$filename = $request->query->get('file');
if (!$filename) {
return new RedirectResponse(url('admin/logs'));
}
// Security check: prevent directory traversal
if (strpos($filename, '..') !== false || strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
return new Response('Invalid filename', 400);
}
$filePath = $this->logPath . '/' . $filename;
if (!file_exists($filePath)) {
return new Response('File not found', 404);
}
// Limit file size read to avoid memory issues (e.g., 5MB)
if (filesize($filePath) > 5 * 1024 * 1024) {
// Read only last 500 lines if file is too big
$content = $this->tailCustom($filePath, 500);
$content = "File too large, showing last 500 lines:\n\n" . $content;
} else {
$content = file_get_contents($filePath);
}
return $this->renderAdminView('logs/show.twig', [
'page_title' => 'Log: ' . $filename,
'filename' => $filename,
'content' => $content
]);
}
/**
* Clear system cache.
*
* @param Request $request
* @return Response
*/
public function clearCache(Request $request): Response
{
$user = $this->getCurrentUser();
if (!$user || !$user->isSuperAdmin()) {
return new Response($this->view->render('components/alerts.twig', [
'type' => 'danger',
'message' => 'Access denied.'
]), 403);
}
$deletedCount = 0;
// Clear file cache
if (is_dir($this->cachePath)) {
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($this->cachePath, \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $fileinfo) {
// Skip .gitignore
if ($fileinfo->getFilename() === '.gitignore') {
continue;
}
$todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink');
if (@$todo($fileinfo->getRealPath())) {
if ($todo === 'unlink') {
$deletedCount++;
}
}
}
}
// Clear OpCache
$opcacheCleared = false;
if (function_exists('opcache_reset')) {
$opcacheCleared = opcache_reset();
}
// Add a success flash message (if flash system exists, otherwise just redirect with query param)
// Since we don't have a flash message system clearly defined in context, let's use query param
return new RedirectResponse(url('dashboard?cache_cleared=1&deleted=' . $deletedCount . '&opcache=' . ($opcacheCleared ? 1 : 0)));
}
private function formatBytes($bytes, $precision = 2) {
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* Slightly modified tail function
*/
private function tailCustom($filepath, $lines = 1) {
$f = @fopen($filepath, "rb");
if ($f === false) return false;
$buffer = 4096;
fseek($f, -1, SEEK_END);
if (fread($f, 1) != "\n") $lines -= 1;
$output = '';
$chunk = '';
while (ftell($f) > 0 && $lines >= 0) {
$seek = min(ftell($f), $buffer);
fseek($f, -$seek, SEEK_CUR);
$chunk = fread($f, $seek);
$output = $chunk . $output;
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
$lines -= substr_count($chunk, "\n");
}
while ($lines++ < 0) {
$output = substr($output, strpos($output, "\n") + 1);
}
fclose($f);
return $output;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Helpers;
/**
* SysCMS - Encryption Helper
*
* Simple AES-256-CBC encryption for sensitive data like API Keys.
*/
class Crypto
{
private static $cipher = "AES-256-CBC";
/**
* Encrypts a string using the APP_KEY as secret
*/
public static function encrypt(string $data): string
{
$key = substr(hash('sha256', env('APP_KEY', 'syscms-default-key')), 0, 32);
$ivlen = openssl_cipher_iv_length(self::$cipher);
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext = openssl_encrypt($data, self::$cipher, $key, $options = 0, $iv);
return base64_encode($iv . $ciphertext);
}
/**
* Decrypts a string
*/
public static function decrypt(string $data): string
{
try {
$key = substr(hash('sha256', env('APP_KEY', 'syscms-default-key')), 0, 32);
$decoded = base64_decode($data);
$ivlen = openssl_cipher_iv_length(self::$cipher);
$iv = substr($decoded, 0, $ivlen);
$ciphertext = substr($decoded, $ivlen);
return openssl_decrypt($ciphertext, self::$cipher, $key, $options = 0, $iv);
} catch (\Exception $e) {
return $data; // Return original if decryption fails (e.g. not encrypted yet)
}
}
}
+132
View File
@@ -0,0 +1,132 @@
<?php
namespace App\Helpers;
use Illuminate\Database\Capsule\Manager as DB;
class DateFormatter
{
private static $defaultFormat = 'd/m/Y';
private static $cachedFormat = null;
/**
* Format a date according to the company's configuration.
*
* @param string|null $date The date string (Y-m-d or Y-m-d H:i:s).
* @param bool $includeTime Whether to include time in the formatted string.
* @return string
*/
public static function format(?string $date, bool $includeTime = false): string
{
if (empty($date)) {
return '';
}
// Try to detect if date is already formatted (simple check for '/')
if (strpos($date, '/') !== false) {
return $date;
}
try {
$format = self::getCompanyDateFormat();
// If the format is just date but time is requested, append time format
if ($includeTime) {
// Heuristic: if format doesn't seem to have time, add it
if (strpos($format, 'H') === false) {
$format .= ' H:i';
}
}
$timestamp = strtotime($date);
if ($timestamp === false) {
return $date; // Return original if parsing fails
}
return date($format, $timestamp);
} catch (\Exception $e) {
// Fallback
return date(self::$defaultFormat . ($includeTime ? ' H:i' : ''), strtotime($date));
}
}
/**
* Get the date format for the current company.
* Caches the result in memory (static property) and session.
*
* @return string
*/
private static function getCompanyDateFormat(): string
{
if (self::$cachedFormat) {
return self::$cachedFormat;
}
// Check session
if (isset($_SESSION['bus_date_format'])) {
self::$cachedFormat = $_SESSION['bus_date_format'];
return self::$cachedFormat;
}
// Fetch from database if user is logged in
if (isset($_SESSION['user_bus_id'])) {
try {
$busId = $_SESSION['user_bus_id'];
$company = DB::table('bus')
->where('bus_id', $busId)
->select('bus_date_format')
->first();
if ($company && !empty($company->bus_date_format)) {
$_SESSION['bus_date_format'] = $company->bus_date_format;
self::$cachedFormat = $company->bus_date_format;
return self::$cachedFormat;
}
} catch (\Exception $e) {
// Log error if needed, but return default for now
}
}
// Fallback default
self::$cachedFormat = self::$defaultFormat;
return self::$cachedFormat;
}
/**
* Converts a formatted date back to MySQL format (Y-m-d).
*
* @param string $date Formatted date string
* @return string|null Y-m-d formatted date or null on failure
*/
public static function toMySQL(string $date): ?string
{
if (empty($date)) {
return null;
}
// If already Y-m-d (contains - and no /)
if (strpos($date, '-') !== false && strpos($date, '/') === false) {
return $date;
}
// Try to parse from company format
$format = self::getCompanyDateFormat();
$d = \DateTime::createFromFormat($format, $date);
if ($d && $d->format($format) === $date) {
return $d->format('Y-m-d');
}
// Try common fallback formats
$formats = ['d/m/Y', 'm/d/Y', 'Y/m/d'];
foreach ($formats as $fmt) {
$d = \DateTime::createFromFormat($fmt, $date);
if ($d && $d->format($fmt) === $date) {
return $d->format('Y-m-d');
}
}
return null;
}
}
+173
View File
@@ -0,0 +1,173 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
use Dotenv\Dotenv;
if (!function_exists('load_env')) {
/**
* Loads environment variables from the .env file
*
* @param string|null $path Path to the project root directory
* @return void
*/
function load_env(?string $path = null): void
{
static $loaded = false;
if ($loaded) {
return;
}
$path = $path ?? dirname(__DIR__, 2);
if (file_exists($path . '/.env')) {
$dotenv = Dotenv::createImmutable($path);
$dotenv->load();
$loaded = true;
}
}
}
if (!function_exists('get_env')) {
/**
* Gets an environment variable with a default value
*
* @param string $key Variable name
* @param mixed $default Default value if not exists
* @return mixed
*/
function get_env(string $key, $default = null)
{
return $_ENV[$key] ?? $_SERVER[$key] ?? $default;
}
}
if (!function_exists('env')) {
/**
* Alias for get_env to maintain compatibility
*
* @param string $key Variable name
* @param mixed $default Default value if not exists
* @return mixed
*/
function env(string $key, $default = null)
{
return get_env($key, $default);
}
}
if (!function_exists('env_bool')) {
/**
* Gets an environment variable as boolean
*
* @param string $key Variable name
* @param bool $default Default value if not exists
* @return bool
*/
function env_bool(string $key, bool $default = false): bool
{
$value = get_env($key, $default);
if (is_bool($value)) {
return $value;
}
return filter_var($value, FILTER_VALIDATE_BOOLEAN);
}
}
if (!function_exists('env_int')) {
/**
* Gets an environment variable as integer
*
* @param string $key Variable name
* @param int $default Default value if not exists
* @return int
*/
function env_int(string $key, int $default = 0): int
{
return (int) get_env($key, $default);
}
}
if (!function_exists('get_db_config')) {
/**
* Gets database configuration from environment variables
*
* @return array
*/
function get_db_config(): array
{
return [
'driver' => get_env('DB_DRIVER', get_env('DB_CONNECTION', 'mysql')),
'host' => get_env('DB_HOST', 'localhost'),
'port' => get_env('DB_PORT', '3306'),
'database' => get_env('DB_DATABASE', 'syscms'),
'username' => get_env('DB_USERNAME', 'root'),
'password' => get_env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
];
}
}
if (!function_exists('get_app_config')) {
/**
* Gets application configuration from environment variables
*
* @return array
*/
function get_app_config(): array
{
return [
'name' => get_env('APP_NAME', 'SysCMS'),
'env' => get_env('APP_ENV', 'development'),
'debug' => env_bool('APP_DEBUG', true),
'url' => get_env('APP_URL', 'http://localhost:16240'),
'timezone' => get_env('APP_TIMEZONE', 'America/Sao_Paulo'),
'key' => get_env('APP_KEY', ''),
];
}
}
if (!function_exists('get_mail_config')) {
/**
* Gets email configuration from environment variables
*
* @return array
*/
function get_mail_config(): array
{
return [
'transport' => get_env('MAIL_TRANSPORT', 'smtp'),
'host' => get_env('MAIL_HOST', 'localhost'),
'port' => get_env('MAIL_PORT', '587'),
'username' => get_env('MAIL_USERNAME', ''),
'password' => get_env('MAIL_PASSWORD', ''),
'encryption' => get_env('MAIL_ENCRYPTION', 'tls'),
'from_address' => get_env('MAIL_FROM_ADDRESS', ''),
'from_name' => get_env('MAIL_FROM_NAME', 'SysCMS'),
];
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace App\Helpers;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
class Log
{
private static $logger;
private static function getLogger()
{
if (!self::$logger) {
self::$logger = new Logger('syscms');
// Logs should be in /logs as per RULES.md
$logPath = __DIR__ . '/../../logs/app.log';
$logDir = dirname($logPath);
if (!is_dir($logDir)) {
mkdir($logDir, 0777, true);
}
self::$logger->pushHandler(new StreamHandler($logPath, Logger::DEBUG));
}
return self::$logger;
}
public static function info($message, array $context = [])
{
self::getLogger()->info($message, $context);
}
public static function error($message, array $context = [])
{
self::getLogger()->error($message, $context);
}
public static function warning($message, array $context = [])
{
self::getLogger()->warning($message, $context);
}
public static function debug($message, array $context = [])
{
self::getLogger()->debug($message, $context);
}
/**
* Specialized write for audit logs
*/
public static function write($message, $module = 'general', $context = [])
{
if (!is_array($context)) {
$context = [];
}
$context['module'] = $module;
self::getLogger()->info($message, $context);
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
namespace App\Helpers;
/**
* Helper para gerenciar o Editor Quill no sistema SysCMS
*/
class QuillHelper
{
/**
* Renderiza um campo de texto com o editor Quill
*
* @param string $name Nome do campo
* @param string $id ID do elemento
* @param string $value Valor inicial
* @param array $options Opções adicionais
* @return string HTML do campo com o editor
*/
public static function renderEditor(string $name, string $id, string $value = '', array $options = []): string
{
// Opções padrão
$defaultOptions = [
'class' => 'quill-editor',
'style' => 'height: 300px;',
'placeholder' => 'Digite seu conteúdo aqui...'
];
// Mesclar opções
$options = array_merge($defaultOptions, $options);
// Construir atributos
$attributes = '';
foreach ($options as $key => $value) {
if ($key !== 'value') {
$attributes .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
}
// Retornar HTML do campo
return sprintf(
'<textarea name="%s" id="%s"%s>%s</textarea>',
htmlspecialchars($name),
htmlspecialchars($id),
$attributes,
htmlspecialchars($value)
);
}
/**
* Renderiza um campo de texto simples (sem Quill)
*
* @param string $name Nome do campo
* @param string $id ID do elemento
* @param string $value Valor inicial
* @param array $options Opções adicionais
* @return string HTML do campo
*/
public static function renderTextarea(string $name, string $id, string $value = '', array $options = []): string
{
// Opções padrão
$defaultOptions = [
'class' => 'form-control',
'rows' => 5
];
// Mesclar opções
$options = array_merge($defaultOptions, $options);
// Construir atributos
$attributes = '';
foreach ($options as $key => $value) {
if ($key !== 'value') {
$attributes .= ' ' . $key . '="' . htmlspecialchars($value) . '"';
}
}
// Retornar HTML do campo
return sprintf(
'<textarea name="%s" id="%s"%s>%s</textarea>',
htmlspecialchars($name),
htmlspecialchars($id),
$attributes,
htmlspecialchars($value)
);
}
/**
* Renderiza um campo de texto com editor (Quill ou textarea simples)
* baseado na configuração do sistema
*
* @param string $name Nome do campo
* @param string $id ID do elemento
* @param string $value Valor inicial
* @param array $options Opções adicionais
* @return string HTML do campo
*/
public static function renderField(string $name, string $id, string $value = '', array $options = []): string
{
// Verificar se o Quill está habilitado
$useQuill = self::isQuillEnabled();
if ($useQuill) {
return self::renderEditor($name, $id, $value, $options);
} else {
return self::renderTextarea($name, $id, $value, $options);
}
}
/**
* Verifica se o editor Quill está habilitado no sistema
*
* @return bool True se o Quill estiver habilitado
*/
public static function isQuillEnabled(): bool
{
// Verificar variável de ambiente
$useQuill = getenv('APP_EDITOR_QUILL') ?: 'true';
return filter_var($useQuill, FILTER_VALIDATE_BOOLEAN);
}
/**
* Obtém a configuração do Quill em formato JSON
*
* @return string Configuração em formato JSON
*/
public static function getQuillConfig(): string
{
$config = [
'theme' => 'snow',
'placeholder' => 'Digite seu conteúdo aqui...',
'modules' => [
'toolbar' => [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ 'header': [1, 2] }],
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
[{ 'script': 'sub'}, { 'script': 'super' }],
[{ 'indent': '-1'}, { 'indent': '+1' }],
[{ 'direction': 'rtl' }],
[{ 'size': ['small', false, 'large', 'huge'] }],
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
[{ 'color': [] }, { 'background': [] }],
[{ 'font': [] }],
[{ 'align': [] }],
['link', 'image'],
['clean']
]
]
];
return json_encode($config);
}
/**
* Renderiza o script de inicialização do Quill
*
* @return string Script de inicialização
*/
public static function renderInitScript(): string
{
if (!self::isQuillEnabled()) {
return '';
}
$config = self::getQuillConfig();
$configJson = json_encode($config);
return <<<HTML
<script>
// Configuração personalizada do Quill
const quillSysConfig = {$configJson};
// Sobrescrever configuração padrão se necessário
if (typeof window.quillDefaultConfig !== 'undefined') {
window.quillDefaultConfig = Object.assign(window.quillDefaultConfig, quillSysConfig);
}
</script>
HTML;
}
/**
* Limpa o conteúdo HTML gerado pelo Quill para segurança
*
* @param string $html Conteúdo HTML
* @return string HTML limpo
*/
public static function sanitizeHtml(string $html): string
{
// Lista de tags permitidas
$allowedTags = '<p><br><strong><em><u><s><h1><h2><h3><h4><h5><h6><ul><ol><li><blockquote><pre><code><a><img>';
// Remover atributos perigosos
$html = strip_tags($html, $allowedTags);
// Remover atributos de estilo e eventos
$html = preg_replace('/\s*(on\w+|style|id|class)=["\'][^"\']*["\']/', '', $html);
return $html;
}
/**
* Converte o conteúdo do Quill para texto puro
*
* @param string $html Conteúdo HTML
* @return string Texto puro
*/
public static function htmlToText(string $html): string
{
// Remover tags HTML
$text = strip_tags($html);
// Converter entidades HTML
$text = html_entity_decode($text, ENT_QUOTES, 'UTF-8');
// Normalizar espaços em branco
$text = preg_replace('/\s+/', ' ', $text);
return trim($text);
}
}
+151
View File
@@ -0,0 +1,151 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Middlewares;
use Closure;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
class PermissionMiddleware
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @param string $permission
* @return mixed
*/
public function handle(Request $request, Closure $next, string $permission)
{
$user = $this->getCurrentUser();
if (!$user) {
return $this->unauthorizedResponse($request);
}
// Check if user has the required permission
if (!$this->userHasPermission($user, $permission)) {
return $this->forbiddenResponse($request, $permission);
}
return $next($request);
}
/**
* Get current authenticated user.
*
* @return \Modules\Auth\Models\User|null
*/
protected function getCurrentUser(): ?\Modules\Auth\Models\User
{
if (!isset($_SESSION['user_id'])) {
return null;
}
try {
return \Modules\Auth\Models\User::find($_SESSION['user_id']);
} catch (\Exception $e) {
error_log("Error loading user: " . $e->getMessage());
return null;
}
}
/**
* Check if user has the required permission.
*
* @param \Modules\Auth\Models\User $user
* @param string $permission
* @return bool
*/
protected function userHasPermission(\Modules\Auth\Models\User $user, string $permission): bool
{
return $user->hasPermission($permission);
}
/**
* Create unauthorized response.
*
* @param Request $request
* @return Response
*/
protected function unauthorizedResponse(Request $request): Response
{
if ($this->expectsJson($request)) {
return new JsonResponse([
'success' => false,
'message' => __('auth.unauthenticated'),
'timestamp' => time()
], 401);
}
return new Response('', 302, [
'Location' => url('/login')
]);
}
/**
* Create forbidden response.
*
* @param Request $request
* @param string $permission
* @return Response
*/
protected function forbiddenResponse(Request $request, string $permission): Response
{
$message = __('auth.permission_denied', ['permission' => $permission]);
if ($this->expectsJson($request)) {
return new JsonResponse([
'success' => false,
'message' => $message,
'permission' => $permission,
'timestamp' => time()
], 403);
}
// For web requests, show error page
try {
$view = new \Kernel\View();
// We can pass the message to the view if we update the template
return new Response($view->render('errors/403.twig'), 403);
} catch (\Throwable $e) {
return new Response($message, 403);
}
}
/**
* Check if request expects JSON response.
*
* @param Request $request
* @return bool
*/
protected function expectsJson(Request $request): bool
{
return $request->headers->get('Accept') === 'application/json' ||
$request->headers->get('Content-Type') === 'application/json' ||
str_contains($request->getPathInfo(), '/api/');
}
}
+385
View File
@@ -0,0 +1,385 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Modules\Company\Models\Company;
use App\Helpers\Log;
use Illuminate\Database\Capsule\Manager as Capsule;
class ActivityLog extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'activity_logs';
/**
* The primary key associated with table.
*
* @var string
*/
protected $primaryKey = 'activity_logs_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'activity_logs_user_id',
'activity_logs_bus_id',
'activity_logs_action',
'activity_logs_resource_type',
'activity_logs_resource_id',
'activity_logs_old_values',
'activity_logs_new_values',
'activity_logs_ip_address',
'activity_logs_user_agent'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'activity_logs_old_values' => 'array',
'activity_logs_new_values' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'created_at',
'updated_at'
];
/**
* Indicates if the model should be timestamped.
*
* @var bool
*/
public $timestamps = true;
/**
* Get the user that performed the activity.
*/
public function user(): BelongsTo
{
return $this->belongsTo(\Modules\Auth\Models\User::class, 'activity_logs_user_id');
}
/**
* Get the company where the activity occurred.
*/
public function company(): BelongsTo
{
return $this->belongsTo(Company::class, 'activity_logs_bus_id');
}
/**
* Scope to get logs for a specific user.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int|null $userId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser($query, ?int $userId)
{
return $query->where('activity_logs_user_id', $userId);
}
/**
* Scope to get logs for a specific company.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int|null $companyId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForCompany($query, ?int $companyId)
{
return $query->where('activity_logs_bus_id', $companyId);
}
/**
* Scope to get logs for a specific action.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $action
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForAction($query, string $action)
{
return $query->where('activity_logs_action', $action);
}
/**
* Scope to get logs for a specific resource type.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $resourceType
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForResourceType($query, string $resourceType)
{
return $query->where('activity_logs_resource_type', $resourceType);
}
/**
* Scope to get logs for a specific resource.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $resourceType
* @param int $resourceId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForResource($query, string $resourceType, int $resourceId)
{
return $query->where('activity_logs_resource_type', $resourceType)
->where('activity_logs_resource_id', $resourceId);
}
/**
* Scope to get logs within a date range.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param \DateTime|string $startDate
* @param \DateTime|string $endDate
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeBetweenDates($query, $startDate, $endDate)
{
return $query->whereBetween('created_at', [$startDate, $endDate]);
}
/**
* Scope to order logs by creation date.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $direction
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOrderByDate($query, string $direction = 'desc')
{
return $query->orderBy('created_at', $direction);
}
/**
* Log an activity.
*
* @param int|null $userId
* @param int|null $companyId
* @param string $action
* @param string $resourceType
* @param int|null $resourceId
* @param array|null $oldValues
* @param array|null $newValues
* @param string|null $ipAddress
* @param string|null $userAgent
* @return static
*/
public static function log(
?int $userId,
?int $companyId,
string $action,
string $resourceType,
?int $resourceId = null,
?array $oldValues = null,
?array $newValues = null,
?string $ipAddress = null,
?string $userAgent = null
): self {
// Also log to file for monitoring
Log::info("Activity: {$action}", [
'user_id' => $userId,
'company_id' => $companyId,
'resource' => "{$resourceType}:{$resourceId}",
'ip' => $ipAddress ?? ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1')
]);
$resolvedCompanyId = $companyId ?? ($_SESSION['current_company_id'] ?? ($_SESSION['user_idbus'] ?? null));
// Validate company id exists to satisfy FK constraints
if ($resolvedCompanyId !== null) {
$exists = Capsule::table('bus')->where('bus_id', $resolvedCompanyId)->exists();
if (!$exists) {
$resolvedCompanyId = null;
}
}
$attributes = [
'activity_logs_user_id' => $userId,
'activity_logs_bus_id' => $resolvedCompanyId,
'activity_logs_action' => $action,
'activity_logs_resource_type' => $resourceType,
'activity_logs_resource_id' => $resourceId,
'activity_logs_old_values' => $oldValues,
'activity_logs_new_values' => $newValues,
'activity_logs_ip_address' => $ipAddress ?? ($_SERVER['REMOTE_ADDR'] ?? '127.0.0.1'),
'activity_logs_user_agent' => $userAgent ?? ($_SERVER['HTTP_USER_AGENT'] ?? 'Console')
];
// If company id is invalid/null and DB enforces NOT NULL or FK, avoid DB insert
if ($resolvedCompanyId === null) {
return new self($attributes);
}
return self::create($attributes);
}
/**
* Get the action attribute.
*
* @return string
*/
public function getActionAttribute(): string
{
return $this->activity_logs_action;
}
/**
* Get the resource type attribute.
*
* @return string
*/
public function getResourceTypeAttribute(): string
{
return $this->activity_logs_resource_type;
}
/**
* Get the resource id attribute.
*
* @return int|null
*/
public function getResourceIdAttribute(): ?int
{
return $this->activity_logs_resource_id;
}
/**
* Get the user name attribute.
*
* @return string
*/
public function getUserNameAttribute(): string
{
return $this->user ? ($this->user->users_name ?: $this->user->users_email) : __('activity.unknown_user');
}
/**
* Get the action color attribute.
*
* @return string
*/
public function getActionColorAttribute(): string
{
$colors = [
'created' => 'success',
'updated' => 'primary',
'deleted' => 'danger',
'login' => 'success',
'logout' => 'warning',
'password_changed' => 'info',
'profile_updated' => 'info',
'module_enabled' => 'success',
'module_disabled' => 'secondary',
'permission_granted' => 'success',
'permission_revoked' => 'danger'
];
return $colors[$this->activity_logs_action] ?? 'secondary';
}
/**
* Get the formatted action description.
*
* @return string
*/
public function getFormattedActionAttribute(): string
{
$actions = [
'created' => __('activity.created'),
'updated' => __('activity.updated'),
'deleted' => __('activity.deleted'),
'login' => __('activity.login'),
'logout' => __('activity.logout'),
'password_changed' => __('activity.password_changed'),
'profile_updated' => __('activity.profile_updated'),
'module_enabled' => __('activity.module_enabled'),
'module_disabled' => __('activity.module_disabled'),
'permission_granted' => __('activity.permission_granted'),
'permission_revoked' => __('activity.permission_revoked')
];
return $actions[$this->activity_logs_action] ?? $this->activity_logs_action;
}
/**
* Get the formatted resource description.
*
* @return string
*/
public function getFormattedResourceAttribute(): string
{
if ($this->activity_logs_resource_type && $this->activity_logs_resource_id) {
return "{$this->activity_logs_resource_type} #{$this->activity_logs_resource_id}";
}
return $this->activity_logs_resource_type ?: __('activity.unknown_resource');
}
/**
* Get the formatted changes description.
*
* @return string
*/
public function getFormattedChangesAttribute(): string
{
if (!$this->activity_logs_old_values && !$this->activity_logs_new_values) {
return '';
}
$changes = [];
if ($this->activity_logs_old_values && $this->activity_logs_new_values) {
foreach ($this->activity_logs_old_values as $key => $oldValue) {
if (isset($this->activity_logs_new_values[$key])) {
$newValue = $this->activity_logs_new_values[$key];
$changes[] = "{$key}: '{$oldValue}' → '{$newValue}'";
}
}
}
return implode(', ', $changes);
}
}
+283
View File
@@ -0,0 +1,283 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Modules\Company\Models\Company;
class Module extends Model
{
/**
* The table associated with model.
*
* @var string
*/
protected $table = 'modules';
/**
* The primary key associated with table.
*
* @var string
*/
protected $primaryKey = 'modules_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'modules_name',
'modules_display_name',
'modules_description',
'modules_version',
'modules_author',
'modules_ative',
'modules_public',
'modules_blocked',
'modules_dependencies',
'modules_permissions',
'modules_settings',
'modules_path',
'modules_namespace',
'modules_icon',
'menu_side_bar',
'menu_nav_bar',
'modules_order'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'modules_ative' => 'boolean',
'modules_public' => 'boolean',
'modules_blocked' => 'boolean',
'menu_side_bar' => 'boolean',
'menu_nav_bar' => 'boolean',
'modules_dependencies' => 'array',
'modules_permissions' => 'array',
'modules_settings' => 'array',
'modules_install_date' => 'datetime',
'modules_update_date' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'modules_install_date',
'modules_update_date',
'created_at',
'updated_at'
];
/**
* Get companies that use this module.
*/
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'bus_modules', 'bus_modules_module_id', 'bus_modules_bus_id')
->withPivot(['bus_modules_is_enabled', 'bus_modules_settings', 'bus_modules_enabled_by', 'bus_modules_enabled_at', 'bus_modules_show_public_nav', 'bus_modules_show_dashboard_nav'])
->withTimestamps();
}
/**
* Scope to get only active modules.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where('modules_ative', true);
}
/**
* Scope to get only public modules.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopePublic($query)
{
return $query->where('modules_public', true);
}
/**
* Scope to get only blocked modules.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeBlocked($query)
{
return $query->where('modules_blocked', true);
}
/**
* Scope to get modules by name.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $name
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeByName($query, string $name)
{
return $query->where('modules_name', $name);
}
/**
* Scope to order modules by order field.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeOrdered($query)
{
return $query->orderBy('modules_order', 'asc');
}
/**
* Check if module is active.
*
* @return bool
*/
public function isActive(): bool
{
return $this->modules_ative;
}
/**
* Check if module is public.
*
* @return bool
*/
public function isPublic(): bool
{
return $this->modules_public;
}
/**
* Check if module is blocked.
*
* @return bool
*/
public function isBlocked(): bool
{
return $this->modules_blocked;
}
/**
* Get module icon with Bootstrap Icons classes.
*
* @return string
*/
public function getIconAttribute(): string
{
return $this->attributes['modules_icon'] ?? 'bi bi-gear';
}
/**
* Get module display name.
*
* @return string
*/
public function getDisplayNameAttribute(): string
{
return $this->modules_display_name ?: $this->modules_name;
}
/**
* Get module dependencies.
*
* @return array
*/
public function getDependenciesAttribute(): array
{
return $this->modules_dependencies ?? [];
}
/**
* Get module permissions.
*
* @return array
*/
public function getPermissionsAttribute(): array
{
return $this->modules_permissions ?? [];
}
/**
* Get module settings.
*
* @return array
*/
public function getSettingsAttribute(): array
{
return $this->modules_settings ?? [];
}
/**
* Get setting value.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function getSetting(string $key, $default = null)
{
$settings = $this->getSettingsAttribute();
return $settings[$key] ?? $default;
}
/**
* Check if module has specific permission.
*
* @param string $permission
* @return bool
*/
public function hasPermission(string $permission): bool
{
return in_array($permission, $this->getPermissionsAttribute());
}
/**
* Check if module depends on another module.
*
* @param string $dependency
* @return bool
*/
public function hasDependency(string $dependency): bool
{
return in_array($dependency, $this->getDependenciesAttribute());
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Modules\Company\Models\Company;
class RolePermission extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'role_permissions';
/**
* The primary key associated with table.
*
* @var string
*/
protected $primaryKey = 'role_permissions_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'role_permissions_role',
'role_permissions_bus_id',
'role_permissions_permission',
'role_permissions_created_by'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'created_at',
'updated_at'
];
/**
* Get the company that owns the role permission.
*/
public function company(): BelongsTo
{
return $this->belongsTo(Company::class, 'role_permissions_bus_id');
}
/**
* Get the user who created the role permission.
*/
public function createdByUser(): BelongsTo
{
return $this->belongsTo(\Modules\Auth\Models\User::class, 'role_permissions_created_by');
}
/**
* Scope to get permissions for a specific role.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $role
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForRole($query, string $role)
{
return $query->where('role_permissions_role', $role);
}
/**
* Scope to get permissions for a specific company.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $companyId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForCompany($query, int $companyId)
{
return $query->where('role_permissions_bus_id', $companyId);
}
/**
* Scope to get permissions with specific permission.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $permission
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeWithPermission($query, string $permission)
{
return $query->where('role_permissions_permission', $permission);
}
}
+178
View File
@@ -0,0 +1,178 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Modules\Company\Models\Company;
use Modules\Auth\Models\User;
use Carbon\Carbon;
class UserPermission extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'user_permissions';
/**
* The primary key associated with table.
*
* @var string
*/
protected $primaryKey = 'user_permissions_id';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'user_permissions_user_id',
'user_permissions_bus_id',
'user_permissions_permission',
'user_permissions_granted_by',
'user_permissions_expires_at'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'user_permissions_granted_at' => 'datetime',
'user_permissions_expires_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime'
];
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = [
'user_permissions_granted_at',
'user_permissions_expires_at',
'created_at',
'updated_at'
];
/**
* Get the user that owns the permission.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_permissions_user_id');
}
/**
* Get the company that owns the permission.
*/
public function company(): BelongsTo
{
return $this->belongsTo(Company::class, 'user_permissions_bus_id');
}
/**
* Get the user who granted the permission.
*/
public function grantedByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'user_permissions_granted_by');
}
/**
* Check if permission is expired.
*
* @return bool
*/
public function isExpired(): bool
{
return $this->user_permissions_expires_at && $this->user_permissions_expires_at->isPast();
}
/**
* Check if permission is active.
*
* @return bool
*/
public function isActive(): bool
{
return !$this->isExpired();
}
/**
* Scope to get only active permissions.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeActive($query)
{
return $query->where(function ($q) {
$q->whereNull('user_permissions_expires_at')
->orWhere('user_permissions_expires_at', '>', Carbon::now());
});
}
/**
* Scope to get permissions for a specific user.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $userId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForUser($query, int $userId)
{
return $query->where('user_permissions_user_id', $userId);
}
/**
* Scope to get permissions for a specific company.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param int $companyId
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeForCompany($query, int $companyId)
{
return $query->where('user_permissions_bus_id', $companyId);
}
/**
* Scope to get permissions with specific permission.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param string $permission
* @return \Illuminate\Database\Eloquent\Builder
*/
public function scopeWithPermission($query, string $permission)
{
return $query->where('user_permissions_permission', $permission);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Services;
use Modules\Company\Models\Company;
class PlatformService
{
/**
* Obtém o nome da plataforma da company com ID 1
* A company ID 1 representa a identidade da plataforma
*/
public function getPlatformName(): string
{
try {
$company = Company::find(1);
if ($company && !empty($company->platform_name) && $company->platform_name !== ':platformName') {
return $company->platform_name;
}
} catch (\Exception $e) {
// Log error silently, return default
}
return 'SysCMS';
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Services;
use Modules\Erpstock\Models\StockMovement;
use Modules\Erpproducts\Models\Product;
use App\Helpers\Log;
use Illuminate\Database\Capsule\Manager as DB;
class ProductStockMovementService
{
/**
* Create a new product stock movement.
*
* @param array $data Movement data
* @param object $user User performing the action
* @return StockMovement The created movement
* @throws \Exception
*/
public function createMovement(array $data, $user)
{
DB::beginTransaction();
try {
$product = Product::where('pr_id', $data['product_id'])
->where('pr_idbus', $user->users_bus_id)
->first();
if (!$product) {
throw new \Exception(__('erpstock.erpstock.movements.error_product_not_found'));
}
$quantity = (float)$data['quantity'];
$type = $data['type']; // 'E' for Entry, 'S' for Exit
// Update product stock
if ($type === 'E') {
$product->pr_estocado += $quantity;
} else {
$product->pr_estocado -= $quantity;
}
$product->save();
// Create movement record
$movementData = [
'mepro_data' => date('Y-m-d'), // Fixed: Legacy system uses DATE type, not DATETIME
'mepro_idpr' => $product->pr_id,
'mepro_idoperador' => $user->users_id,
'mepro_tipoes' => $type,
'mepro_qtdade' => $quantity,
'mepro_historico' => $data['history'] ?? '',
'mepro_documento' => $data['document'] ?? '',
'mepro_lote' => $data['batch'] ?? '',
'mepro_custo' => $data['cost'] ?? 0,
'mepro_preco' => $data['price'] ?? 0,
'mepro_ipi' => $data['ipi'] ?? 0,
'mepro_icms' => $data['icms'] ?? 0,
'mepro_oimpostos' => $data['other_taxes'] ?? 0,
'mepro_obs' => $data['observation'] ?? '',
'mepro_idbus' => $user->users_bus_id,
'mepro_idfornecedor' => $data['supplier_id'] ?? 0,
'mepro_idcliente' => $data['customer_id'] ?? 0,
'mepro_idarmazem' => $data['warehouse_id'] ?? 0,
'mepro_nfantasia' => $product->pr_descricao, // Fixed: Product model doesn't have pr_nome
'mepro_descricao' => $product->pr_descricao,
'mepro_codigo' => $product->pr_codigo,
'mepro_exp1' => $data['extra1'] ?? '',
'mepro_exp2' => $data['extra2'] ?? '',
'mepro_exp3' => $data['extra3'] ?? '',
// Legacy required fields with default values
'mepro_idhistorico' => 0,
'mepro_idoc' => 0,
'mepro_idcoc' => 0,
'mepro_idcot' => 0,
'mepro_pn' => '',
'mepro_bp' => ''
];
$movement = StockMovement::create($movementData);
Log::write('erpstock', 'Product Stock Movement', "Created movement ID: {$movement->mepro_id} with history: " . ($movementData['mepro_historico'] ?? 'NULL'));
DB::commit();
return $movement;
} catch (\Exception $e) {
DB::rollBack();
Log::write('erpstock', 'Error', 'Failed to create movement: ' . $e->getMessage());
throw $e;
}
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Services;
use Modules\Erprawmaterials\Models\RawMaterial;
use Modules\ErpStock\Models\RawMaterialStockMovement;
use Illuminate\Database\Capsule\Manager as DB;
use App\Helpers\Log;
class RawMaterialStockMovementService
{
/**
* Create a stock movement for a raw material.
*
* @param array $data Normalized data for the movement
* @param object $user The current user
* @return RawMaterialStockMovement
* @throws \Exception
*/
public function createMovement(array $data, $user)
{
// Validate required fields
if (empty($data['mp_id'])) {
throw new \Exception("Raw Material ID is required.");
}
// Find the raw material
$material = RawMaterial::where('mp_id', $data['mp_id'])
->where('mp_idbus', $user->users_bus_id)
->first();
if (!$material) {
throw new \Exception("Raw Material not found.");
}
$quantity = $data['quantity'] ?? 0;
if ($quantity <= 0) {
throw new \Exception("Invalid quantity: must be greater than zero.");
}
$type = $data['type'] ?? 'E'; // E = Entry, S = Exit
// Prepare data for model
$movementData = [
'memp_idbus' => $user->users_bus_id,
'memp_idmp' => $material->mp_id,
'memp_data' => date('Y-m-d'),
'memp_idoperador' => $user->users_id,
'memp_tipoes' => $type,
'memp_qtdade' => $quantity,
'memp_historico' => $data['history'] ?? '',
'memp_documento' => $data['document'] ?? '',
'memp_lote' => $data['batch'] ?? '',
'memp_custo' => $data['cost'] ?? 0,
'memp_preco' => $data['price'] ?? 0,
'memp_ipi' => $data['ipi'] ?? 0,
'memp_icms' => $data['icms'] ?? 0,
'memp_oimpostos' => $data['other_taxes'] ?? 0,
'memp_obs' => $data['observation'] ?? '',
'memp_codigo' => $material->mp_codigo,
'memp_nome' => $material->mp_descricao,
'memp_idfornecedor' => $data['supplier_id'] ?? 0,
'memp_idcliente' => $data['customer_id'] ?? 0,
'memp_idarmazem' => $data['warehouse_id'] ?? 0,
];
// Begin transaction
DB::beginTransaction();
try {
// Create movement record using the ErpStock model which includes warehouse support
$movement = RawMaterialStockMovement::create($movementData);
// Update raw material stock level
if ($type === 'E') {
$material->mp_estocado += $quantity;
} else {
$material->mp_estocado -= $quantity;
}
$material->save();
DB::commit();
Log::write("Movimentação de estoque ({$type}) para: {$material->mp_descricao} (Qtd: {$quantity})", 'erprawmaterials');
return $movement;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Dotenv\Dotenv;
use Illuminate\Database\Capsule\Manager as Capsule;
// Load environment variables
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv::createImmutable(__DIR__);
$dotenv->load();
}
// Helper function to get environment variables
if (!function_exists('get_env')) {
function get_env(string $key, $default = null)
{
return $_ENV[$key] ?? $_SERVER[$key] ?? $default;
}
}
// Helper function to get database configuration
if (!function_exists('get_db_config')) {
function get_db_config(): array
{
return [
'driver' => get_env('DB_DRIVER', get_env('DB_CONNECTION', 'mysql')),
'host' => get_env('DB_HOST', 'localhost'),
'port' => get_env('DB_PORT', '3306'),
'database' => get_env('DB_DATABASE', 'syscms'),
'username' => get_env('DB_USERNAME', 'root'),
'password' => get_env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
];
}
}
// Initialize Capsule (Eloquent)
$capsule = new Capsule;
$dbConfig = get_db_config();
$capsule->addConnection([
'driver' => $dbConfig['driver'],
'host' => $dbConfig['host'],
'port' => $dbConfig['port'],
'database' => $dbConfig['database'],
'username' => $dbConfig['username'],
'password' => $dbConfig['password'],
'charset' => $dbConfig['charset'],
'collation' => $dbConfig['collation'],
'prefix' => $dbConfig['prefix'],
]);
// Set the event dispatcher used by Eloquent models... (optional)
// use Illuminate\Events\Dispatcher;
// use Illuminate\Container\Container;
// $capsule->setEventDispatcher(new Dispatcher(new Container));
// Make this Capsule instance available globally via static methods... (optional)
$capsule->setAsGlobal();
// Setup the Eloquent ORM... (optional; unless you've used setEventDispatcher())
$capsule->bootEloquent();
echo "Conectado ao banco de dados: " . $dbConfig['database'] . " em " . $dbConfig['host'] . ":" . $dbConfig['port'] . "\n";
try {
// Remove entries from 'modules' table
echo "Removendo módulos 'affiliates' e 'affiliatesadmin' da tabela 'modules'...\n";
$deletedModules = Capsule::table('modules')
->whereIn('name', ['affiliates', 'affiliatesadmin'])
->delete();
echo "Removidos $deletedModules módulos da tabela 'modules'.\n";
// Remove entries from 'bus_modules' table
echo "Removendo módulos 'affiliates' e 'affiliatesadmin' da tabela 'bus_modules'...\n";
$deletedBusModules = Capsule::table('bus_modules')
->whereIn('module_name', ['affiliates', 'affiliatesadmin'])
->delete();
echo "Removidos $deletedBusModules módulos da tabela 'bus_modules'.\n";
// Drop affiliate tables
echo "Removendo tabelas de afiliados...\n";
$tablesToDrop = [
'affiliate_clicks',
'affiliate_banners',
'affiliate_commissions',
'affiliates',
];
foreach ($tablesToDrop as $table) {
if (Capsule::schema()->hasTable($table)) {
Capsule::schema()->drop($table);
echo "Tabela '$table' removida.\n";
} else {
echo "Tabela '$table' não existe, pulando.\n";
}
}
echo "Processo de remoção de dados e tabelas de afiliados concluído com sucesso.\n";
} catch (\Exception $e) {
echo "Erro durante a remoção: " . $e->getMessage() . "\n";
}
?>
+288
View File
@@ -0,0 +1,288 @@
<?php
/**
* SysCMS - Content Management System
*
* Copyright (c) 2025 Marco Costa
* All rights reserved.
*
* Author: Marco Costa
* Email: contato@syspanel.com.br
* Project: SysCMS
*
* This software is licensed, not sold.
* Permission is granted to use this software
* subject to the terms of the SysCMS Commercial License.
*
* Redistribution, sublicensing, resale, or
* disclosure of this software, in whole or in part,
* is strictly prohibited without prior written permission.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
*/
// Loads the environment helper first
require_once __DIR__ . '/Helpers/Environment.php';
use Kernel\Translator;
use App\Helpers\Log;
use Kernel\Application;
if (!function_exists('__')) {
/**
* Translate a string using the translator
*
* @param string $key The translation key
* @param array $params Parameters to replace in the translation
* @return string The translated string
*/
function __(string $key, array $params = []): string
{
return Translator::translate($key, $params);
}
}
if (!function_exists('add_action')) {
function add_action(string $hook, callable $listener, int $priority = 10): void
{
Application::getInstance()->getHookManager()->addAction($hook, $listener, $priority);
}
}
if (!function_exists('do_action')) {
function do_action(string $hook, ...$args): void
{
Application::getInstance()->getHookManager()->doAction($hook, ...$args);
}
}
if (!function_exists('add_filter')) {
function add_filter(string $hook, callable $listener, int $priority = 10): void
{
Application::getInstance()->getHookManager()->addFilter($hook, $listener, $priority);
}
}
if (!function_exists('apply_filters')) {
function apply_filters(string $hook, $value, ...$args)
{
return Application::getInstance()->getHookManager()->applyFilters($hook, $value, ...$args);
}
}
if (!function_exists('get_active_theme')) {
/**
* Get the currently active theme name
*/
function get_active_theme(): string
{
static $activeTheme = null;
if ($activeTheme === null) {
try {
$themeConfig = \Illuminate\Database\Capsule\Manager::table('system_config')
->where('system_config_key', 'active_theme')
->first();
$activeTheme = $themeConfig ? $themeConfig->system_config_value : 'default';
} catch (\Exception $e) {
$activeTheme = 'default';
}
}
return $activeTheme;
}
}
if (!function_exists('asset_url')) {
/**
* Generate asset URL with theme support
*
* @param string $path The asset path
* @return string The complete asset URL
*/
function asset_url(string $path): string
{
$activeTheme = get_active_theme();
$themeAssetPath = 'themes/' . $activeTheme . '/assets/' . ltrim($path, '/');
// Check if the asset exists in the theme folder
if (file_exists(dirname(__DIR__) . '/' . $themeAssetPath)) {
return '/' . $themeAssetPath;
}
return '/assets/' . ltrim($path, '/');
}
}
if (!function_exists('url')) {
/**
* Generate application URL
*
* @param string $path The path to append to the base URL
* @return string The complete URL
*/
function url(string $path = ''): string
{
return '/' . ltrim($path, '/');
}
}
if (!function_exists('route')) {
/**
* Generate route URL (placeholder for future implementation)
*
* @param string $name The route name
* @param array $params Route parameters
* @return string The route URL
*/
function route(string $name, array $params = []): string
{
// This will be implemented when we have a named route system
return url($name);
}
}
if (!function_exists('csrf_field')) {
/**
* Generate CSRF token field
*
* @return string HTML input with CSRF token
*/
function csrf_field(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$token = $_SESSION['csrf_token'];
return '<input type="hidden" name="csrf_token" value="' . htmlspecialchars($token) . '">';
}
}
if (!function_exists('csrf_token')) {
/**
* Get CSRF token value
*
* @return string The CSRF token
*/
function csrf_token(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
}
if (!function_exists('verify_csrf_token')) {
/**
* Verify CSRF token
*
* @param string $token The token to verify
* @return bool True if valid, false otherwise
*/
function verify_csrf_token(string $token): bool
{
return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
}
}
if (!function_exists('logger')) {
/**
* Log a message to the application log
*
* @param string $message The message to log
* @param array $context Contextual information
* @param string $level Log level (info, error, warning, debug)
*/
function logger(string $message, array $context = [], string $level = 'info'): void
{
switch ($level) {
case 'error':
Log::error($message, $context);
break;
case 'warning':
Log::warning($message, $context);
break;
case 'debug':
Log::debug($message, $context);
break;
default:
Log::info($message, $context);
break;
}
}
}
if (!function_exists('is_multi_lang_enabled')) {
/**
* Check if multi-language is enabled
*
* @return bool True if multi-language is enabled, false otherwise
*/
function is_multi_lang_enabled(): bool
{
return ($_ENV['APP_MULTI_LANG'] ?? 'false') === 'true';
}
}
if (!function_exists('bus_module_enabled')) {
/**
* Check if a module is enabled for the current authenticated user's company.
*
* @param string $moduleName The module name to check (e.g., 'erppoe')
* @return bool
*/
function bus_module_enabled(string $moduleName): bool
{
if (session_status() === PHP_SESSION_NONE) {
return false;
}
if (!isset($_SESSION['user_id'])) {
return false;
}
try {
$user = \Modules\Auth\Models\User::find($_SESSION['user_id']);
if (!$user) {
return false;
}
if ($user->users_role === 'super_admin') {
return \App\Models\Module::where('modules_name', $moduleName)
->where('modules_ative', true)
->exists();
}
return \App\Models\Module::join('bus_modules', 'modules.modules_id', '=', 'bus_modules.bus_modules_module_id')
->where('bus_modules.bus_modules_bus_id', $user->users_bus_id)
->where('bus_modules.bus_modules_is_enabled', true)
->where('modules.modules_name', $moduleName)
->where('modules.modules_ative', true)
->exists();
} catch (\Exception $e) {
return false;
}
}
}
if (!function_exists('get_config')) {
/**
* Get a system configuration value from the database.
*
* @param string $key The configuration key.
* @param mixed $default Default value if the key is not found.
* @return mixed
*/
function get_config(string $key, $default = null)
{
try {
$config = \Illuminate\Database\Capsule\Manager::table('system_config')
->where('system_config_key', $key)
->first();
return $config ? $config->system_config_value : $default;
} catch (\Exception $e) {
return $default;
}
}
}
+249
View File
@@ -0,0 +1,249 @@
/* Tema Default - SysCMS */
/* Variáveis CSS */
:root {
--primary-color: #667eea;
--secondary-color: #764ba2;
--font-family: 'Roboto', sans-serif;
--card-border-radius: 15px;
--button-border-radius: 50px;
--transition-duration: 0.3s;
--hero-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
/* Estilos Globais */
body {
font-family: var(--font-family);
line-height: 1.6;
color: #333;
}
/* Seção Hero */
.hero-section {
background: var(--hero-gradient);
color: white;
padding: 100px 0;
margin-bottom: 50px;
}
.hero-title {
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 20px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.hero-subtitle {
font-size: 1.5rem;
margin-bottom: 30px;
opacity: 0.9;
}
.btn-hero {
padding: 15px 30px;
font-size: 1.2rem;
border-radius: var(--button-border-radius);
font-weight: 600;
transition: all var(--transition-duration) ease;
}
.btn-hero:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0,0,0,0.2);
}
.btn-outline-hero {
background: transparent;
border: 2px solid white;
color: white;
margin-left: 15px;
}
.btn-outline-hero:hover {
background: white;
color: var(--primary-color);
}
/* Cards de Recursos */
.feature-card {
transition: transform var(--transition-duration) ease, box-shadow var(--transition-duration) ease;
border: none;
border-radius: var(--card-border-radius);
overflow: hidden;
height: 100%;
}
.feature-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.15);
}
.feature-icon {
font-size: 3rem;
margin-bottom: 20px;
color: var(--primary-color);
}
/* Seção de Estatísticas */
.stats-section {
background-color: #f8f9fa;
padding: 80px 0;
}
.stat-item {
text-align: center;
margin-bottom: 30px;
}
.stat-number {
font-size: 3rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 10px;
}
/* Cabeçalho de Página */
.page-header {
background: var(--hero-gradient);
color: white;
padding: 80px 0;
margin-bottom: 50px;
}
/* Seção Sobre */
.about-section {
padding: 50px 0;
}
.team-member {
margin-bottom: 30px;
text-align: center;
}
.team-member img {
width: 150px;
height: 150px;
border: 5px solid #fff;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
/* Cards de Serviços */
.service-card {
transition: all var(--transition-duration) ease;
border: none;
border-radius: var(--card-border-radius);
overflow: hidden;
}
.service-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
}
.service-icon {
font-size: 3rem;
margin-bottom: 20px;
color: var(--primary-color);
}
/* Cards de Preços */
.price-card {
border: none;
border-radius: var(--card-border-radius);
transition: all var(--transition-duration);
}
.price-card:hover {
box-shadow: 0 15px 30px rgba(0,0,0,0.1);
transform: scale(1.02);
}
.price-header {
background-color: #f8f9fa;
padding: 30px;
border-bottom: 1px solid #eee;
}
.price-body {
padding: 40px;
}
.price-amount {
font-size: 3rem;
font-weight: bold;
color: #333;
}
/* Seção de Títulos */
.section-title {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 20px;
text-align: center;
position: relative;
padding-bottom: 15px;
}
.section-title:after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 4px;
background: var(--primary-color);
border-radius: 2px;
}
.section-subtitle {
text-align: center;
max-width: 700px;
margin: 0 auto 60px;
font-size: 1.1rem;
color: #6c757d;
}
/* Seção CTA */
.cta-section {
background: var(--hero-gradient);
color: white;
padding: 80px 0;
text-align: center;
}
/* Classes Específicas do Tema */
.theme-default {
/* Estilos específicos do tema podem ser adicionados aqui */
}
.page-home {
/* Estilos específicos para a página home */
}
.page-about {
/* Estilos específicos para a página sobre */
}
.page-services {
/* Estilos específicos para a página de serviços */
}
/* Responsividade */
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.2rem;
}
.section-title {
font-size: 2rem;
}
.stat-number {
font-size: 2.5rem;
}
}
+224
View File
@@ -0,0 +1,224 @@
/**
* Default Theme - JavaScript
* SysCMS - Content Management System
*/
document.addEventListener('DOMContentLoaded', function() {
// Theme initialization
initTheme();
// Initialize animated counters
initAnimatedCounters();
// Initialize scroll animations
initScrollAnimations();
// Initialize hover effects
initHoverEffects();
});
/**
* Initializes the theme
*/
function initTheme() {
// Add theme class to body
document.body.classList.add('theme-default');
// Check for custom theme configurations
applyThemeConfig();
}
/**
* Applies custom theme configurations
*/
function applyThemeConfig() {
// Here we could apply dynamic theme configurations
// For example, custom colors, fonts, etc.
// Example of how to apply custom colors if they exist
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary-color');
const secondaryColor = getComputedStyle(document.documentElement).getPropertyValue('--secondary-color');
if (primaryColor && primaryColor.trim() !== '') {
document.documentElement.style.setProperty('--primary-color', primaryColor.trim());
}
if (secondaryColor && secondaryColor.trim() !== '') {
document.documentElement.style.setProperty('--secondary-color', secondaryColor.trim());
}
}
/**
* Initializes animated counters for statistics
*/
function initAnimatedCounters() {
const counters = document.querySelectorAll('.stat-number');
const speed = 200;
const countUp = function() {
counters.forEach(counter => {
const target = +counter.getAttribute('data-count');
const count = +counter.innerText;
const increment = target / speed;
if (count < target) {
counter.innerText = Math.ceil(count + increment);
setTimeout(countUp, 10);
} else {
counter.innerText = target;
}
});
};
// Trigger animation when statistics section is visible
const statsSection = document.querySelector('.stats-section');
if (statsSection) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
countUp();
observer.unobserve(entry.target);
}
});
}, { threshold: 0.5 });
observer.observe(statsSection);
}
}
/**
* Initializes scroll animations
*/
function initScrollAnimations() {
// Animation for elements appearing on scroll
const animatedElements = document.querySelectorAll('.feature-card, .service-card, .price-card, .team-member');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.style.opacity = '1';
entry.target.style.transform = 'translateY(0)';
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
animatedElements.forEach(element => {
// Configure initial state
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
element.style.transition = 'opacity 0.6s ease, transform 0.6s ease';
// Observe element
observer.observe(element);
});
}
/**
* Initializes hover effects
*/
function initHoverEffects() {
// Parallax effect in hero section
const heroSection = document.querySelector('.hero-section');
if (heroSection) {
window.addEventListener('scroll', () => {
const scrolled = window.pageYOffset;
const parallax = heroSection.querySelector('img');
if (parallax) {
const speed = 0.5;
parallax.style.transform = `translateY(${scrolled * speed}px)`;
}
});
}
// Elevation effect on cards
const cards = document.querySelectorAll('.feature-card, .service-card, .price-card');
cards.forEach(card => {
card.addEventListener('mouseenter', function() {
this.style.transform = 'translateY(-10px)';
});
card.addEventListener('mouseleave', function() {
this.style.transform = 'translateY(0)';
});
});
}
/**
* Utility function to animate elements
*/
function animateElement(element, animation, duration = 1000) {
element.style.animation = `${animation} ${duration}ms ease-in-out`;
setTimeout(() => {
element.style.animation = '';
}, duration);
}
/**
* Utility function to add class with delay
*/
function addClassWithDelay(element, className, delay = 100) {
setTimeout(() => {
element.classList.add(className);
}, delay);
}
/**
* Function to create background particles (optional)
*/
function createBackgroundParticles() {
const particlesContainer = document.createElement('div');
particlesContainer.className = 'particles-container';
particlesContainer.style.position = 'fixed';
particlesContainer.style.top = '0';
particlesContainer.style.left = '0';
particlesContainer.style.width = '100%';
particlesContainer.style.height = '100%';
particlesContainer.style.pointerEvents = 'none';
particlesContainer.style.zIndex = '-1';
particlesContainer.style.overflow = 'hidden';
// Create particles
for (let i = 0; i < 50; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.position = 'absolute';
particle.style.width = Math.random() * 5 + 'px';
particle.style.height = particle.style.width;
particle.style.backgroundColor = 'rgba(255, 255, 255, 0.5)';
particle.style.borderRadius = '50%';
particle.style.left = Math.random() * 100 + '%';
particle.style.top = Math.random() * 100 + '%';
particle.style.opacity = Math.random() * 0.5 + 0.2;
// Float animation
const duration = Math.random() * 20 + 10;
const delay = Math.random() * 5;
particle.style.animation = `float ${duration}s ${delay}s infinite ease-in-out`;
particlesContainer.appendChild(particle);
}
// Add CSS for float animation
const style = document.createElement('style');
style.textContent = `
@keyframes float {
0%, 100% { transform: translateY(0) translateX(0); }
25% { transform: translateY(-20px) translateX(10px); }
50% { transform: translateY(10px) translateX(-10px); }
75% { transform: translateY(-10px) translateX(20px); }
}
`;
document.head.appendChild(style);
document.body.appendChild(particlesContainer);
}
// Export functions for global use
window.ThemeDefault = {
animateElement,
addClassWithDelay,
createBackgroundParticles
};
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env php
<?php
/**
* SysCMS Console Wrapper
* This file is maintained for backward compatibility.
* Please use the 'syscli' file in the root directory.
*/
require_once __DIR__ . '/../syscli';
+39
View File
@@ -0,0 +1,39 @@
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use Kernel\Application;
$app = new Application();
// Run Migrations
$migrations = [
__DIR__ . '/../modules/eventsguide/database/migrations/001_create_events_table.php',
__DIR__ . '/../modules/eventsguide/database/migrations/002_create_event_gallery_table.php',
__DIR__ . '/../modules/eventsguide/database/migrations/003_create_event_reports_table.php',
__DIR__ . '/../modules/eventsguide/database/migrations/004_create_event_ads_table.php',
__DIR__ . '/../modules/eventsguide/database/migrations/005_insert_module_record.php',
];
foreach ($migrations as $file) {
if (file_exists($file)) {
require_once $file;
// Extract class name from file content
$content = file_get_contents($file);
if (preg_match('/class\s+(\w+)/', $content, $matches)) {
$className = $matches[1];
if (class_exists($className)) {
echo "Running $className...\n";
$migration = new $className();
$migration->up();
echo "$className done.\n";
} else {
echo "Class $className not found in $file\n";
}
}
} else {
echo "File not found: $file\n";
}
}
echo "Installation complete.\n";
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More