first commit

This commit is contained in:
Claudecio Martins
2025-11-04 18:22:02 +01:00
commit c1184d2878
4394 changed files with 444123 additions and 0 deletions

35
vendor/claudecio/axiumphp/src/AxiumPHP.php vendored Executable file
View File

@@ -0,0 +1,35 @@
<?php
namespace AxiumPHP;
use Exception;
class AxiumPHP {
private array $requiredConstants = [
'INI_SYSTEM_PATH',
'STORAGE_FOLDER_PATH'
];
/**
* Construtor que vai garantir que as constantes necessárias estejam definidas antes de
* instanciar o AxiumPHP.
*/
public function __construct() {
// Verificar as constantes no momento da criação da instância
$this->checkRequiredConstants();
}
/**
* Verifica se todas as constantes necessárias estão definidas.
*
* @throws Exception Se alguma constante necessária não estiver definida.
*/
private function checkRequiredConstants(): void {
foreach ($this->requiredConstants as $constant) {
if (!defined(constant_name: $constant)) {
throw new Exception(message: "Constante '{$constant}' não definida.");
}
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace AxiumPHP\Core;
use PDO;
use Dotenv\Dotenv;
use RuntimeException;
use AxiumPHP\Core\Database\Drivers\MySQL;
use AxiumPHP\Core\Database\Drivers\Postgres;
class Database {
private static array $connections = [];
private static bool $envLoaded = false;
private static function loadEnv(): void {
if (!self::$envLoaded) {
if (file_exists(filename: ROOT_SYSTEM_PATH . '/.env')) {
$dotenv = Dotenv::createImmutable(paths: ROOT_SYSTEM_PATH);
$dotenv->load();
}
self::$envLoaded = true;
}
}
/**
* Retorna conexão por nome + schema
*/
public static function getConnection(string $connectionName = 'DEFAULT', ?string $schema = null): mixed {
self::loadEnv();
$connectionName = strtoupper(string: $connectionName);
$key = $connectionName . ($schema ? "_$schema" : '');
if (!isset(self::$connections[$key])) {
$driver = $_ENV["{$connectionName}_DATABASE_DRIVER"];
switch (strtolower(string: $driver)) {
case 'mysql':
$conn = new MySQL(envName: $connectionName);
break;
case 'pgsql':
$conn = new Postgres(envName: $connectionName, schema: $schema);
break;
default:
throw new RuntimeException(message: "Driver desconhecido: {$driver}");
}
self::$connections[$key] = $conn;
}
return self::$connections[$key];
}
public static function disconnect(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$connectionName = strtoupper(string: $connectionName);
$key = $connectionName . ($schema ? "_$schema" : '');
self::$connections[$key] = null;
}
/** Métodos auxiliares (execute, fetchAll, fetchOne, etc.) */
public static function execute(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): bool {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'execute')) {
return $conn->execute($sql, $params);
}
throw new RuntimeException(message: "O driver '{$connectionName}' não suporta execute()");
}
public static function fetchAll(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): array {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'fetchAll')) {
return $conn->fetchAll($sql, $params);
}
throw new RuntimeException("O driver '{$connectionName}' não suporta fetchAll()");
}
public static function fetchOne(string $sql, array $params = [], string $connectionName = 'DEFAULT', ?string $schema = null): ?array {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'fetchOne')) {
return $conn->fetchOne($sql, $params);
}
throw new RuntimeException(message: "O driver '{$connectionName}' não suporta fetchOne()");
}
// Transações
public static function beginTransaction(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'beginTransaction')) $conn->beginTransaction();
}
public static function commit(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if ($conn instanceof PDO && $conn->inTransaction()) {
$conn->commit();
} elseif (method_exists(object_or_class: $conn, method: 'getPDO')) {
$pdo = $conn->getPDO();
if ($pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->commit();
}
}
}
public static function rollback(string $connectionName = 'DEFAULT', ?string $schema = null): void {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if ($conn instanceof PDO && $conn->inTransaction()) {
$conn->rollback();
} elseif (method_exists(object_or_class: $conn, method: 'getPDO')) {
$pdo = $conn->getPDO();
if ($pdo instanceof PDO && $pdo->inTransaction()) {
$pdo->rollback();
}
}
}
public static function lastInsertId(string $connectionName = 'DEFAULT', ?string $schema = null): string {
$conn = self::getConnection(connectionName: $connectionName, schema: $schema);
if (method_exists(object_or_class: $conn, method: 'getPDO')) return $conn->getPDO()->lastInsertId();
throw new RuntimeException(message: "O driver '{$connectionName}' não suporta lastInsertId()");
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace AxiumPHP\Core\Database\Drivers;
use PDO;
use PDOException;
use RuntimeException;
class MySQL extends PDOAbstract {
protected function connect(string $envName): void {
$host = $_ENV["{$envName}_DATABASE_HOST"];
$port = $_ENV["{$envName}_DATABASE_PORT"] ?? 3306;
$dbname = $_ENV["{$envName}_DATABASE_NAME"];
$user = $_ENV["{$envName}_DATABASE_USERNAME"];
$password = $_ENV["{$envName}_DATABASE_PASSWORD"];
$charset = $_ENV["{$envName}_DATABASE_CHARSET"] ?? 'utf8mb4';
try {
$this->connection = new PDO(
dsn: "mysql:host={$host};port={$port};dbname={$dbname};charset={$charset}",
username: $user,
password: $password,
options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_PERSISTENT => true,
]
);
} catch (PDOException $e) {
throw new RuntimeException(message: "Erro ao conectar no MySQL: {$e->getMessage()}");
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace AxiumPHP\Core\Database\Drivers;
use PDO;
use RuntimeException;
abstract class PDOAbstract {
protected PDO $connection;
public function __construct(string $envName) {
$this->connect(envName: $envName);
}
abstract protected function connect(string $envName): void;
public function execute(string $sql, array $params = []): bool {
$stmt = $this->connection->prepare(query: $sql);
return $stmt->execute(params: $params);
}
public function fetchAll(string $sql, array $params = []): array {
$stmt = $this->connection->prepare(query: $sql);
$stmt->execute(params: $params);
return $stmt->fetchAll(mode: PDO::FETCH_ASSOC);
}
public function fetchOne(string $sql, array $params = []): ?array {
$stmt = $this->connection->prepare(query: $sql);
$stmt->execute(params: $params);
$result = $stmt->fetch(mode: PDO::FETCH_ASSOC);
return $result ?: null;
}
public function beginTransaction(): void {
if (!$this->connection->inTransaction()) {
$this->connection->beginTransaction();
}
}
public function commit(): void {
if ($this->connection->inTransaction()) {
$this->connection->commit();
}
}
public function rollback(): void {
if ($this->connection->inTransaction()) {
$this->connection->rollBack();
}
}
public function getPDO(): PDO {
return $this->connection;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace AxiumPHP\Core\Database\Drivers;
use PDO;
use PDOException;
use RuntimeException;
class Postgres extends PDOAbstract {
public function __construct(string $envName, ?string $schema = null) {
$this->connect(envName: $envName, schema: $schema);
}
protected function connect(string $envName, ?string $schema = null): void {
$host = $_ENV["{$envName}_DATABASE_HOST"];
$port = $_ENV["{$envName}_DATABASE_PORT"] ?? 5432;
$dbname = $_ENV["{$envName}_DATABASE_NAME"];
$user = $_ENV["{$envName}_DATABASE_USERNAME"];
$pass = $_ENV["{$envName}_DATABASE_PASSWORD"];
$schema = $schema ?? ($_ENV["{$envName}_DATABASE_SCHEMA"] ?? 'public');
$systemTimeZone = $_ENV["SYSTEM_TIMEZONE"] ?? 'UTC';
$sslMode = $_ENV["{$envName}_DATABASE_SSLMODE"] ?? 'disable'; // disable, require, verify-ca, verify-full
$dsn = "pgsql:host={$host};port={$port};dbname={$dbname};sslmode={$sslMode}";
try {
$this->connection = new PDO(
dsn: $dsn,
username: $user,
password: $pass,
options: [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_PERSISTENT => false
]
);
// Define o fuso horário da sessão
$this->connection->exec(statement: "SET TIME ZONE '{$systemTimeZone}'");
// Define o schema padrão
$this->connection->exec(statement: "SET search_path TO {$schema}, public");
} catch (PDOException $e) {
throw new RuntimeException(
message: "Erro ao conectar no PostgreSQL: {$e->getMessage()}"
);
}
}
// Método auxiliar para alterar schema dinamicamente
public function setSchema(string $schema): void {
$this->connection->exec(statement: "SET search_path TO {$schema}, public");
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace AxiumPHP\Core;
use DateTime;
use Exception;
use AxiumPHP\Core\Database;
class LoggerService {
public const DRIVER_FILE = 'FILE';
public const DRIVER_DATABASE = 'DATABASE';
private static string $logDir;
private static bool $initialized = false;
private static string $driver = self::DRIVER_FILE;
private static string $connectionName = 'DEFAULT'; // conexão do Database
/**
* Inicializa o Logger
*/
public static function init(string $driver = self::DRIVER_FILE,?string $logDir = null,string $dbConnectionName = 'DEFAULT'): void {
self::$driver = strtoupper(string: $driver);
self::$connectionName = $dbConnectionName;
if (!defined(constant_name: 'STORAGE_FOLDER_PATH')) {
throw new Exception(message: "Constante 'STORAGE_FOLDER_PATH' não foi definida.");
}
self::$logDir = $logDir ?? STORAGE_FOLDER_PATH . '/logs';
if (self::$driver === self::DRIVER_DATABASE) {
// Garante que a conexão exista
Database::getConnection(connectionName: self::$connectionName);
}
if (self::$driver === self::DRIVER_FILE && !is_dir(filename: self::$logDir)) {
if (!mkdir(directory: self::$logDir, permissions: 0775, recursive: true) && !is_dir(filename: self::$logDir)) {
throw new Exception(message: "Não foi possível criar o diretório de logs: " . self::$logDir);
}
}
self::$initialized = true;
}
/**
* Log genérico
*/
public static function log(string $message, string $level = 'INFO', array $context = []): void {
if (!self::$initialized) {
throw new Exception(message: "LoggerService não foi inicializado. Chame LoggerService::init() antes.");
}
switch (self::$driver) {
case self::DRIVER_FILE:
self::logToFile(message: $message, level: $level);
break;
case self::DRIVER_DATABASE:
self::logToDatabase(message: $message, level: $level, context: $context);
break;
}
}
// Métodos auxiliares por nível
public static function info(string $message, array $context = []): void {
self::log(message: $message, level: 'INFO', context: $context);
}
public static function warning(string $message, array $context = []): void {
self::log(message: $message, level: 'WARNING', context: $context);
}
public static function error(string $message, array $context = []): void {
self::log(message: $message, level: 'ERROR', context: $context);
}
public static function debug(string $message, array $context = []): void {
self::log(message: $message, level: 'DEBUG', context: $context);
}
/**
* Log para arquivo
*/
private static function logToFile(string $message, string $level): void {
$date = (new DateTime())->format(format: 'Y-m-d');
$now = (new DateTime())->format(format: 'Y-m-d H:i:s');
$filename = self::$logDir . "/app-{$date}.log";
$logMessage = "[$now][$level] $message" . PHP_EOL;
file_put_contents(filename: $filename, data: $logMessage, flags: FILE_APPEND);
}
/**
* Log para database usando o Database multi-driver
*/
private static function logToDatabase(string $message, string $level, array $context = []): void {
$sql =
"INSERT INTO logs (
level,
message,
context,
created_at
) VALUES (
:level,
:message,
:context,
:created_at
)";
Database::execute(
sql: $sql,
params: [
'level' => $level,
'message' => $message,
'context' => json_encode(value: $context),
'created_at' => (new DateTime())->format(format: 'Y-m-d H:i:s')
],
connectionName: self::$connectionName
);
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace AxiumPHP\Core\Module;
use Exception;
class Loader {
private $configFilePath;
private $configData;
private $startedModules = [];
private static array $loadedShortcuts = [];
private static array $loadedModules = [];
private array $requiredConstants = [
'MODULE_PATH',
];
private $modulePath = MODULE_PATH;
private $iniSystemPath = INI_SYSTEM_PATH;
/**
* Construtor da classe.
*
* Inicializa o objeto com o caminho do arquivo de configuração e carrega as configurações.
*
* @param string $configFileName O caminho do arquivo de configuração (opcional).
*/
public function __construct(?string $configFileName = "system-ini.json") {
$this->configFilePath = "{$this->iniSystemPath}/{$configFileName}";
$this->loadConfig();
// Verificar as constantes no momento da criação da instância
$this->checkRequiredConstants();
}
/**
* Verifica se todas as constantes necessárias estão definidas.
*
* @throws Exception Se alguma constante necessária não estiver definida.
*/
private function checkRequiredConstants(): void {
foreach ($this->requiredConstants as $constant) {
if (!defined(constant_name: $constant)) {
throw new Exception(message: "Constante '{$constant}' não definida.");
}
}
}
/**
* Carrega as configurações do arquivo JSON.
*
* Este método lê o conteúdo do arquivo de configuração especificado em
* `$this->configFilePath`, decodifica-o de JSON e armazena os dados
* no atributo `$this->configData`.
*
* @throws Exception Se o arquivo de configuração não for encontrado.
*
* @return void
*/
private function loadConfig():void {
$configPath = $this->configFilePath;
// Verifica se o arquivo de configuração existe
if (file_exists(filename: $configPath)) {
$jsonContent = file_get_contents(filename: $configPath);
$this->configData = json_decode(json: $jsonContent, associative: true);
} else {
throw new Exception(message: "Arquivo de inicialização não encontrado: {$configPath}");
}
}
/**
* Carrega os módulos do núcleo do sistema definidos na configuração.
*
* Este método carrega os módulos listados na seção "Core" do arquivo
* de configuração, utilizando o método `startModule`.
*
* @return void
*/
public function loadCoreModules():void {
$this->startModule(modules: $this->configData["Modules"]["Core"]);
}
/**
* Carrega e inicializa todos os módulos ativos da aplicação, conforme
* definido na configuração.
*
* Este método acessa a propriedade `$configData`, especificamente a seção
* ["Modules"]["active"], que se espera ser um array contendo os
* identificadores (nomes ou objetos) dos módulos que devem ser carregados
* e inicializados. Em seguida, chama o método `startModule`, passando este
* array de módulos ativos para iniciar o processo de carregamento e
* inicialização de cada um deles.
*
* @return void
*
* @see startModule()
* @property array $configData Propriedade da classe que contém os dados de
* configuração da aplicação. Espera-se que possua uma chave "Modules" com
* uma sub-chave "active" contendo um array de módulos.
*/
public function loadActiveModules():void {
$this->startModule(modules: $this->configData["Modules"]["Active"]);
}
/**
* Carrega e inicializa um único módulo da aplicação.
*
* Este método recebe um identificador de módulo (que pode ser uma string
* com o nome do módulo ou um objeto representando o módulo) e o passa para
* o método `startModule` para iniciar o processo de carregamento e
* inicialização desse módulo específico.
*
* @param mixed $module O identificador do módulo a ser carregado. Pode ser
* uma string contendo o nome do módulo ou um objeto de módulo já instanciado.
* O tipo exato depende da implementação do sistema de módulos.
*
* @return void
*
* @see startModule()
*/
public function loadModule(mixed $module) {
$this->startModule(modules: [$module]);
}
/**
* Retorna os atalhos carregados para um slug de módulo específico.
*
* Este método estático permite acessar atalhos que foram previamente carregados
* e armazenados na propriedade estática `self::$loadedShortcuts`. Ele espera
* o slug (identificador amigável) de um módulo como entrada e retorna o array
* de atalhos associado a ele.
*
* @param string $moduleSlug O slug do módulo para o qual os atalhos devem ser retornados.
* @return array|null Um array contendo os atalhos para o módulo especificado, ou `null`
* se nenhum atalho tiver sido carregado para aquele slug.
*/
public static function getShortcuts(string $moduleSlug): ?array {
if(isset(self::$loadedShortcuts[$moduleSlug]['shortcuts'])) {
return self::$loadedShortcuts[$moduleSlug]['shortcuts'];
}
return null;
}
/**
* Retorna os slugs dos módulos que foram carregados.
*
* Este método estático fornece acesso à lista de identificadores (slugs)
* de todos os módulos que foram previamente carregados e armazenados na
* propriedade estática `self::$loadedModules`. É útil para verificar
* quais módulos estão ativos ou disponíveis no contexto atual da aplicação.
*
* @return array|null Um array contendo os slugs dos módulos carregados, ou `null`
* se nenhum módulo tiver sido carregado ou se a propriedade estiver vazia.
*/
public static function getSlugLoadedModules(): ?array {
return self::$loadedModules;
}
/**
* Busca o nome real de uma subpasta dentro de um diretório base,
* ignorando a diferença entre maiúsculas e minúsculas.
*
* Este método privado recebe um `$basePath` (o diretório onde procurar)
* e um `$targetName` (o nome da pasta desejada). Primeiro, verifica se o
* `$basePath` é um diretório válido. Se não for, retorna null. Em seguida,
* lê todos os arquivos e pastas dentro do `$basePath`. Para cada entrada,
* compara o nome da entrada com o `$targetName` de forma case-insensitive.
* Se encontrar uma entrada que corresponda ao `$targetName` (ignorando o case)
* e que seja um diretório, retorna o nome da entrada com o seu case real.
* Se após verificar todas as entradas nenhuma pasta correspondente for
* encontrada, retorna null.
*
* @param string $basePath O caminho para o diretório base onde a subpasta será procurada.
* @param string $targetName O nome da subpasta a ser encontrada (a comparação é case-insensitive).
* @return string|null O nome real da pasta (com o case correto) se encontrada, ou null caso contrário.
*/
private function getRealFolderName(string $basePath, string $targetName): ?string {
if (!is_dir(filename: $basePath)) return null;
$entries = scandir(directory: $basePath);
foreach ($entries as $entry) {
if (strcasecmp(string1: $entry, string2: $targetName) === 0 && is_dir(filename: "{$basePath}/{$entry}")) {
return $entry; // Nome com o case real
}
}
return null;
}
/**
* Inicia os módulos especificados.
*
* Este método itera sobre a lista de módulos fornecida, carrega seus manifestos,
* verifica a compatibilidade da versão, carrega as dependências e inclui o
* arquivo de bootstrap do módulo.
*
* @param array $modules Um array de strings representando os módulos a serem iniciados.
* Cada string deve estar no formato "nome_do_modulo@versao".
*
* @throws Exception Se o manifesto do módulo não for encontrado, se houver um erro ao decodificar
* o manifesto, se a versão do módulo for incompatível ou se o bootstrap
* do módulo não for encontrado.
*
* @return void
*/
private function startModule(array $modules): void {
foreach ($modules as $module) {
// Identifica o módulo requisitado e sua versão
[$moduleName, $version] = explode(separator: '@', string: $module);
// Pega o nome real da pasta do módulo
$realModuleFolder = $this->getRealFolderName(basePath: $this->modulePath, targetName: $moduleName);
if (!$realModuleFolder) {
throw new Exception(message: "Pasta do módulo '{$moduleName}' não encontrada.");
}
// Caminho do manifesto usando o nome real
$manifestPath = "{$this->modulePath}/{$realModuleFolder}/manifest.json";
if (!file_exists(filename: $manifestPath)) {
throw new Exception(message: "Manifesto do módulo '{$moduleName}' não encontrado.");
}
$moduleManifest = json_decode(json: file_get_contents(filename: $manifestPath), associative: true);
if (!$moduleManifest) {
throw new Exception(message: "Erro ao decodificar o manifesto do módulo '{$moduleName}'.");
}
// Evita carregar o mesmo módulo mais de uma vez
if (in_array(needle: $moduleManifest["uuid"], haystack: $this->startedModules)) {
continue;
}
// Verifica se a versão é compatível
if ($moduleManifest['version'] !== $version) {
throw new Exception(message: "Versão do módulo '{$moduleName}' é incompatível. Versão requerida: {$version}. Versão instalada: {$moduleManifest['version']}");
}
// Inicia Bootstrap do Módulo
$bootstrapModuleFile = "{$this->modulePath}/{$realModuleFolder}/bootstrap.php";
if (file_exists(filename: $bootstrapModuleFile)) {
require_once $bootstrapModuleFile;
}
// Procura a pasta Routes com o case correto
$realRoutesFolder = $this->getRealFolderName(basePath: "{$this->modulePath}/{$realModuleFolder}", targetName: 'Routes');
if ($realRoutesFolder) {
// Procura arquivo com atalhos de rotas
$shortcutsFile = "{$this->modulePath}/{$realModuleFolder}/{$realRoutesFolder}/shortcuts.json";
if (file_exists(filename: $shortcutsFile)) {
$shortcuts = json_decode(
json: file_get_contents(filename: $shortcutsFile),
associative: true
);
self::$loadedShortcuts[$moduleManifest["slug"]] = $shortcuts;
}
}
// Marca como carregado
$this->startedModules[] = $moduleManifest["uuid"];
self::$loadedModules[] = $moduleManifest["slug"];
// Carrega dependências, se existirem
if (!empty($moduleManifest['dependencies'])) {
$this->startModule(modules: $moduleManifest['dependencies']);
}
}
}
}

635
vendor/claudecio/axiumphp/src/Core/Router.php vendored Executable file
View File

@@ -0,0 +1,635 @@
<?php
namespace AxiumPHP\Core;
use Exception;
class Router {
private static $routes = [];
private static $params = [];
private static $ROUTER_MODE = null;
private static $APP_SYS_MODE = null;
private static $ROUTER_ALLOWED_ORIGINS = ['*'];
private static $currentGroupPrefix = '';
private static $currentGroupMiddlewares = [];
private static array $requiredConstants = [
'ROUTER_MODE',
'APP_SYS_MODE'
];
private static array $allowedHttpRequests = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'OPTIONS'
];
/**
* Construtor que vai garantir que as constantes necessárias estejam definidas antes de
* instanciar a view.
*/
public function __construct() {
// Verificar as constantes no momento da criação da instância
$this->checkRequiredConstant();
// Define constante
self::$ROUTER_MODE = strtoupper(string: ROUTER_MODE);
self::$APP_SYS_MODE = strtoupper(string: APP_SYS_MODE);
// Se o modo de roteamento for JSON, define as origens permitidas
if(self::$ROUTER_MODE === 'JSON') {
self::$ROUTER_ALLOWED_ORIGINS = ROUTER_ALLOWED_ORIGINS;
}
}
/**
* Retorna o modo de roteamento atual.
*
* Este método estático simplesmente retorna o valor da propriedade estática `self::$ROUTER_MODE`.
* Ele é usado para obter o modo de operação do roteador, que pode indicar se está
* em modo de desenvolvimento, produção ou outro modo configurado.
*
* @return string O modo de roteamento como uma string.
*/
public static function getMode(): string {
return (string) self::$ROUTER_MODE;
}
/**
* Verifica se todas as constantes necessárias estão definidas.
*
* @throws Exception Se alguma constante necessária não estiver definida.
*/
private function checkRequiredConstant(): void {
foreach (self::$requiredConstants as $constant) {
if (!defined(constant_name: $constant)) {
http_response_code(response_code: 500);
header(header: "Content-Type: application/json; charset=utf-8");
echo json_encode(value: [
"status" => 'error',
"message" => "Constante '{$constant}' não definida.",
]);
exit;
}
}
}
/**
* Adiciona uma rota com método GET à lista de rotas da aplicação.
*
* Este método é um atalho para adicionar rotas com o método HTTP GET. Ele
* chama o método `addRoute` internamente, passando os parâmetros
* fornecidos e o método 'GET'.
*
* @param string $uri O caminho da rota (ex: '/usuarios', '/produtos').
* @param array $handler Um array contendo o nome do controlador e o nome da ação
* que devem ser executados quando a rota for
* corresponder (ex: ['UsuarioController', 'index']).
* @param array $middlewares Um array opcional contendo os nomes dos middlewares que
* devem ser executados antes do handler da rota.
*
* @return void
*/
public static function GET(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: "GET", uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma rota com método POST à lista de rotas da aplicação.
*
* Este método é um atalho para adicionar rotas com o método HTTP POST. Ele
* chama o método `addRoute` internamente, passando os parâmetros
* fornecidos e o método 'POST'.
*
* @param string $uri O caminho da rota (ex: '/usuarios', '/produtos').
* @param array $handler Um array contendo o nome do controlador e o nome da ação
* que devem ser executados quando a rota for
* corresponder (ex: ['UsuarioController', 'salvar']).
* @param array $middlewares Um array opcional contendo os nomes dos middlewares que
* devem ser executados antes do handler da rota.
*
* @return void
*/
public static function POST(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: "POST", uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma rota com método PUT à lista de rotas da aplicação.
*
* Este método é um atalho para adicionar rotas com o método HTTP PUT. Ele
* chama o método `addRoute` internamente, passando os parâmetros
* fornecidos e o método 'PUT'.
*
* @param string $uri O caminho da rota (ex: '/usuarios', '/produtos').
* @param array $handler Um array contendo o nome do controlador e o nome da ação
* que devem ser executados quando a rota for
* corresponder (ex: ['UsuarioController', 'salvar']).
* @param array $middlewares Um array opcional contendo os nomes dos middlewares que
* devem ser executados antes do handler da rota.
*
* @return void
*/
public static function PUT(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: "PUT", uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma nova rota que responde a requisições PATCH.
*
* Este é um método estático de conveniência que simplifica o registro de rotas
* para o método HTTP PATCH. Ele delega a tarefa principal para o método
* privado `addRoute`, passando o método HTTP, a URI, a função de manipulação
* (`handler`) e quaisquer middlewares associados.
*
* @param string $uri A URI da rota (ex: '/api/resource/{id}').
* @param array $handler Um array de dois elementos que define o manipulador da rota: o nome da classe do controlador e o nome do método a ser executado.
* @param array $middlewares Um array opcional de middlewares a serem executados antes do manipulador da rota.
* @return void
*/
public static function PATCH(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: "PATCH", uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma rota com método DELETE à lista de rotas da aplicação.
*
* Este método é um atalho para adicionar rotas com o método HTTP DELETE. Ele
* chama o método `addRoute` internamente, passando os parâmetros
* fornecidos e o método 'DELETE'.
*
* @param string $uri O caminho da rota (ex: '/usuarios', '/produtos').
* @param array $handler Um array contendo o nome do controlador e o nome da ação
* que devem ser executados quando a rota for
* corresponder (ex: ['UsuarioController', 'salvar']).
* @param array $middlewares Um array opcional contendo os nomes dos middlewares que
* devem ser executados antes do handler da rota.
*
* @return void
*/
public static function DELETE(string $uri, array $handler, array $middlewares = []): void {
self::addRoute(method: "DELETE", uri: $uri, handler: $handler, middlewares: $middlewares);
}
/**
* Adiciona uma rota à lista de rotas da aplicação.
*
* Este método estático armazena informações sobre uma rota (método HTTP,
* caminho, controlador, ação e middlewares) em um array interno `$routes`
* para posterior processamento pelo roteador.
*
* @param string $method O método HTTP da rota (ex: 'GET', 'POST', 'PUT', 'DELETE').
* @param string $uri O caminho da rota (ex: '/usuarios', '/produtos/:id').
* @param array $handler Um array contendo o nome do controlador e o nome da ação
* que devem ser executados quando a rota for
* corresponder (ex: ['UsuarioController', 'index']).
* @param array $middlewares Um array opcional contendo os nomes dos middlewares que
* devem ser executados antes do handler da rota.
*
* @return void
*/
private static function addRoute(string $method, string $uri, array $handler, array $middlewares = []): void {
self::$routes[] = [
'method' => strtoupper(string: $method),
'path' => '/' . trim(string: self::$currentGroupPrefix . '/' . trim(string: $uri, characters: '/'), characters: '/'),
'controller' => $handler[0],
'action' => $handler[1],
'middlewares' => array_merge(self::$currentGroupMiddlewares, $middlewares)
];
}
/**
* Verifica se um caminho de rota corresponde a um caminho de requisição.
*
* Este método estático compara um caminho de rota definido (ex: '/usuarios/:id')
* com um caminho de requisição (ex: '/usuarios/123'). Ele suporta parâmetros
* de rota definidos entre chaves (ex: ':id', ':nome'). Os parâmetros
* correspondentes do caminho de requisição são armazenados no array estático
* `$params` da classe.
*
* @param string $routePath O caminho da rota a ser comparado.
* @param string $requestPath O caminho da requisição a ser comparado.
*
* @return bool True se o caminho da requisição corresponder ao caminho da
* rota, false caso contrário.
*/
private static function matchPath($routePath, $requestPath): bool {
// Limpa os parâmetros antes de capturar novos
self::$params = []; // Certifica que a cada nova tentativa, a lista de parâmetros começa vazia
$routeParts = explode(separator: '/', string: trim(string: $routePath, characters: '/'));
$requestParts = explode(separator: '/', string: trim(string: $requestPath, characters: '/'));
if (count(value: $routeParts) !== count(value: $requestParts)) {
return false;
}
foreach ($routeParts as $i => $part) {
if (preg_match(pattern: '/^{\w+}$/', subject: $part)) {
self::$params[] = $requestParts[$i];
} elseif ($part !== $requestParts[$i]) {
return false;
}
}
return true;
}
/**
* Agrupa rotas sob um prefixo e middlewares.
*
* Este método estático permite agrupar rotas que compartilham um prefixo de
* caminho e/ou middlewares. O prefixo e os middlewares definidos dentro do
* grupo serão aplicados a todas as rotas definidas dentro da função de
* callback.
*
* @param string $prefix O prefixo a ser adicionado aos caminhos das rotas
* dentro do grupo (ex: '/admin', '/api/v1').
* @param callable $callback Uma função anônima (callback) que define as rotas
* que pertencem a este grupo.
* @param array $middlewares Um array opcional contendo os middlewares que devem
* ser aplicados a todas as rotas dentro do
* grupo.
*
* @return void
*/
public static function group(string $prefix, callable $callback, array $middlewares = []): void {
$previousPrefix = self::$currentGroupPrefix ?? '';
$previousMiddlewares = self::$currentGroupMiddlewares ?? [];
self::$currentGroupPrefix = $previousPrefix . $prefix;
self::$currentGroupMiddlewares = array_merge($previousMiddlewares, $middlewares);
call_user_func(callback: $callback);
self::$currentGroupPrefix = $previousPrefix;
self::$currentGroupMiddlewares = $previousMiddlewares;
}
/**
* Extrai os dados do corpo da requisição HTTP.
*
* Este método estático é responsável por analisar e retornar os dados enviados
* no corpo de uma requisição HTTP, especialmente para métodos como `PUT` e `DELETE`,
* onde os dados não são automaticamente populados em superglobais como `$_POST` ou `$_GET`.
*
* O fluxo de extração é o seguinte:
* 1. **Lê o Corpo da Requisição:** `file_get_contents('php://input')` lê o conteúdo bruto do corpo da requisição HTTP.
* 2. **Processamento Condicional:**
* * **Para métodos `PUT` ou `DELETE`:**
* * Verifica se o corpo da requisição (`$inputData`) não está vazio.
* * **Verifica o `Content-Type`:**
* * Se o `Content-Type` for `application/json`, tenta decodificar o corpo da requisição como JSON.
* * Se houver um erro na decodificação JSON, ele define o código de status HTTP para 500,
* envia uma resposta JSON com uma mensagem de erro e encerra a execução.
* * Se o `Content-Type` não for `application/json` (ou não estiver definido), tenta analisar o corpo da requisição
* como uma string de query URL (`parse_str`), o que é comum para `application/x-www-form-urlencoded`
* em requisições `PUT`/`DELETE`.
* * **Para outros métodos (e.g., `POST`, `GET`):** O método não tenta extrair dados do `php://input`.
* Nesses casos, presume-se que os dados já estariam disponíveis em `$_POST`, `$_GET`, etc.
* 3. **Limpeza Final:** Remove a chave `_method` dos dados, se presente. Essa chave é frequentemente
* usada em formulários HTML para simular métodos `PUT` ou `DELETE` através de um campo oculto.
*
* @param string $method O método HTTP da requisição (e.g., 'GET', 'POST', 'PUT', 'DELETE').
* @return array Um array associativo contendo os dados extraídos do corpo da requisição.
* Em caso de erro de decodificação JSON, a execução é encerrada com uma resposta de erro HTTP.
*/
private static function extractRequestData(string $method): array {
$inputData = file_get_contents(filename: 'php://input');
$data = [];
// Só processa se for PUT ou DELETE e tiver dados no corpo
if (in_array(needle: $method, haystack: ['PUT', 'DELETE', 'PATCH']) && !empty($inputData)) {
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
// Se for JSON
if (str_contains(haystack: $contentType, needle: 'application/json')) {
$data = json_decode(json: $inputData, associative: true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(response_code: 500);
header(header: "Content-Type: application/json; charset=utf-8");
$json_error = json_last_error_msg();
echo json_encode(value: [
"status" => 'error',
"message" => "Erro ao decodificar JSON: {$json_error}"
]);
exit;
}
}
// Se for outro tipo (ex: x-www-form-urlencoded)
else {
parse_str(string: $inputData, result: $data);
}
}
// Remove o _method se tiver
if(isset($data['_method'])) {
unset($data['_method']);
}
return $data;
}
/**
* Executa os middlewares.
*
* Este método estático itera sobre um array de middlewares e executa cada um
* deles. Os middlewares podem ser especificados como 'Classe::metodo' ou
* 'Classe::metodo:argumento1:argumento2' para passar argumentos para o
* método do middleware. Se um middleware retornar `false`, a execução é
* interrompida e a função retorna `false`.
*
* @param array $middlewares Um array contendo os middlewares a serem executados.
*
* @return bool True se todos os middlewares forem executados com sucesso,
* false se algum middleware falhar.
* @throws Exception Se o formato do middleware for inválido ou se o método
* do middleware não existir.
*/
public static function runMiddlewares(array $middlewares): bool {
foreach ($middlewares as $middleware) {
if (strpos(haystack: $middleware, needle: '::') !== false) {
[$middlewareClass, $methodWithArgs] = explode(separator: '::', string: $middleware);
// Suporte a argumentos no middleware (ex: Middleware::Permission:ADMIN)
$methodParts = explode(separator: ':', string: $methodWithArgs);
$method = $methodParts[0];
$args = array_slice(array: $methodParts, offset: 1);
if (method_exists(object_or_class: $middlewareClass, method: $method)) {
$result = call_user_func_array(callback: [$middlewareClass, $method], args: $args);
// Se retornar false -> erro genérico
if ($result === false) {
http_response_code(response_code: 403);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => "Middleware {$middlewareClass}::{$method} bloqueou a requisição."
]);
return false;
}
// Se retornar array com status 'success' -> mostra a mensagem
if (is_array(value: $result) && ($result['status'] ?? '') !== 'success') {
http_response_code(response_code: $result['response_code'] ?? 403);
header(header: 'Content-Type: application/json; charset=utf-8');
$response = [
"response_code" => $result['response_code'] ?? 403,
"status" => $result['status'] ?? 'error',
"message" => $result['message'] ?? 'Erro no middleware'
];
if(isset($result['output'])) {
$response['output'] = $result['output'];
}
echo json_encode(value: $response);
return false;
}
} else {
http_response_code(response_code: 500);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => "Método '{$method}' não existe na classe '{$middlewareClass}'"
]);
exit;
}
} else {
http_response_code(response_code: 500);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => "Formato inválido do middleware: '{$middleware}'"
]);
exit;
}
}
return true; // todos os middlewares passaram
}
/**
* Verifica se uma rota corresponde à requisição.
*
* Este método verifica se o método HTTP e o caminho da requisição correspondem
* aos da rota fornecida.
*
* @param string $method O método HTTP da requisição.
* @param string $uri O caminho da requisição.
* @param array $route Um array associativo contendo os dados da rota.
*
* @return bool Retorna true se a rota corresponder, false caso contrário.
*/
private static function matchRoute(string $method, string $uri, array $route) {
// Verifica se o método HTTP da rota corresponde ao da requisição
if ($route['method'] !== $method) {
return false;
}
// Verifica se o caminho da requisição corresponde ao caminho da rota
return self::matchPath(routePath: $route['path'], requestPath: $uri);
}
/**
* Prepara os parâmetros para um método de requisição.
*
* Este método combina os parâmetros da rota, os parâmetros GET e, para
* requisições PUT ou DELETE, os parâmetros fornecidos, retornando um
* array de parâmetros preparados.
*
* @param string $method O método HTTP da requisição (GET, POST, PUT, DELETE).
* @param array|null $params Um array opcional de parâmetros adicionais.
*
* @return array Um array contendo os parâmetros preparados.
*/
private static function prepareMethodParameters(string $method, ?array $params = []): array {
// Guarda os parâmetros atuais (da rota)
$atuallParams = self::$params;
// Adiciona os dados de PUT/DELETE/PATCH como um array no final
if (in_array(needle: $method, haystack: ['PUT', 'DELETE', 'PATCH'])) {
self::$params = array_merge($atuallParams, $params);
}
// Normaliza os parâmetros para um array sequencial
$preparedParams = array_values(array: self::$params);
return $preparedParams;
}
/**
* Despacha a requisição para o controlador e ação correspondentes.
*
* Este método estático analisa a requisição (método HTTP e caminho), encontra
* a rota correspondente na lista de rotas definidas, executa os middlewares
* da rota (se houver), instancia o controlador e chama a ação (método)
* especificada na rota, passando os parâmetros da requisição (parâmetros da
* rota, parâmetros GET e dados de PUT/DELETE). Se nenhuma rota
* corresponder, um erro 404 é enviado.
*
* @return void
*/
public static function dispatch(): void {
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url(url: $_SERVER['REQUEST_URI'], component: PHP_URL_PATH);
$uri = trim(string: rtrim(string: $uri, characters: '/'), characters: '/');
// Suporte ao _method em POST para PUT/DELETE/PATCH
if ($method === 'POST' && isset($_POST['_method'])) {
$method = strtoupper(string: $_POST['_method']);
}
// Verifica se o método HTTP é permitido
if(!in_array(needle: $method, haystack: self::$allowedHttpRequests)) {
http_response_code(response_code: 405);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => "Método HTTP '{$method}' não permitido."
]);
exit;
}
// =============================
// HEADERS CORS PARA TODOS OS MÉTODOS
// =============================
if (self::$ROUTER_MODE === 'JSON') {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
if (in_array(needle: '*', haystack: self::$ROUTER_ALLOWED_ORIGINS)) {
header(header: "Access-Control-Allow-Origin: *");
} elseif (in_array(needle: $origin, haystack: self::$ROUTER_ALLOWED_ORIGINS)) {
header(header: "Access-Control-Allow-Origin: $origin");
} else {
if (self::$APP_SYS_MODE === 'DEV') {
header(header: "Access-Control-Allow-Origin: $origin");
} else {
http_response_code(response_code: 403);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: [
"status" => 'error',
"message" => "Origem '{$origin}' não permitida pelo CORS."
]);
exit;
}
}
header(header: 'Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header(header: 'Access-Control-Allow-Headers: Content-Type, Authorization');
}
// =============================
// TRATAMENTO DO PRE-FLIGHT (OPTIONS)
// =============================
if ($method === 'OPTIONS') {
http_response_code(response_code: 204); // No Content
exit;
}
// =============================
// EXTRAI DADOS DE PUT, DELETE, PATCH
// =============================
$requestData = match($method) {
'GET' => null,
'POST' => null,
default => self::extractRequestData(method: $method)
};
// =============================
// LOOP PARA PROCESSAR ROTAS
// =============================
foreach (self::$routes as $route) {
if (self::matchRoute(method: $method, uri: $uri, route: $route)) {
// Executa middlewares
if (!empty($route['middlewares']) && !self::runMiddlewares(middlewares: $route['middlewares'])) {
return; // Middleware bloqueou
}
$controller = new $route['controller']();
$action = $route['action'];
$params = self::prepareMethodParameters(method: $method, params: [$requestData]);
switch (self::$ROUTER_MODE) {
case 'VIEW':
if (method_exists(object_or_class: $controller, method: $action)) {
http_response_code(response_code: 200);
call_user_func_array(callback: [$controller, $action], args: $params);
exit;
}
break;
case 'JSON':
if (method_exists(object_or_class: $controller, method: $action)) {
http_response_code(response_code: 200);
header(header: 'Content-Type: application/json; charset=utf-8');
call_user_func_array(callback: [$controller, $action], args: $params);
exit;
}
break;
}
}
}
// =============================
// ROTA NÃO ENCONTRADA
// =============================
self::pageNotFound();
exit;
}
/**
* Exibe a página de erro 404 (Página não encontrada).
*
* Este método estático define o código de resposta HTTP como 404 e renderiza
* a view "/Errors/404" para exibir a página de erro. Após a renderização,
* o script é encerrado.
*
* @return void
*/
private static function pageNotFound(): void {
switch (self::$ROUTER_MODE) {
case 'VIEW':
// Notifica erro em caso constante não definida
if(!defined(constant_name: 'ERROR_404_VIEW_PATH')) {
http_response_code(response_code: 500);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode( value: [
"status" => 'error',
"message" => "Constante 'ERROR_404_VIEW_PATH' não foi definida.",
]);
exit;
}
// Caso o arquivo da constante não exista, notifica erro
if(!file_exists(filename: ERROR_404_VIEW_PATH)) {
http_response_code(response_code: 500);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode( value: [
"status" => 'error',
"message" => "Arquivo da constante 'ERROR_404_VIEW_PATH' não foi encontrado.",
]);
exit;
}
http_response_code(response_code: 404);
require_once ERROR_404_VIEW_PATH;
break;
case 'JSON':
http_response_code(response_code: 404);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode( value: [
"status" => 'error',
"message" => "Página não encontrada.",
]);
break;
}
}
}

140
vendor/claudecio/axiumphp/src/Core/View.php vendored Executable file
View File

@@ -0,0 +1,140 @@
<?php
namespace AxiumPHP\Core;
use Exception;
class View {
private array $requiredConstants = [
'VIEW_PATH',
];
/**
* Construtor que vai garantir que as constantes necessárias estejam definidas antes de
* instanciar a view.
*/
public function __construct() {
// Verificar as constantes no momento da criação da instância
$this->checkRequiredConstants();
}
/**
* Verifica se todas as constantes necessárias estão definidas.
*
* @throws Exception Se alguma constante necessária não estiver definida.
*/
private function checkRequiredConstants(): void {
foreach ($this->requiredConstants as $constant) {
if (!defined(constant_name: $constant)) {
throw new Exception(message: "Constante '{$constant}' não definida.");
}
}
}
/**
* Busca o nome real de uma subpasta dentro de um diretório base,
* ignorando a diferença entre maiúsculas e minúsculas.
*
* Este método privado recebe um `$basePath` (o diretório onde procurar)
* e um `$targetName` (o nome da pasta desejada). Primeiro, verifica se o
* `$basePath` é um diretório válido. Se não for, retorna null. Em seguida,
* lê todos os arquivos e pastas dentro do `$basePath`. Para cada entrada,
* compara o nome da entrada com o `$targetName` de forma case-insensitive.
* Se encontrar uma entrada que corresponda ao `$targetName` (ignorando o case)
* e que seja um diretório, retorna o nome da entrada com o seu case real.
* Se após verificar todas as entradas nenhuma pasta correspondente for
* encontrada, retorna null.
*
* @param string $basePath O caminho para o diretório base onde a subpasta será procurada.
* @param string $targetName O nome da subpasta a ser encontrada (a comparação é case-insensitive).
* @return string|null O nome real da pasta (com o case correto) se encontrada, ou null caso contrário.
*/
private static function getRealFolderName(string $basePath, string $targetName): ?string {
if (!is_dir(filename: $basePath)) return null;
$entries = scandir(directory: $basePath);
foreach ($entries as $entry) {
if (strcasecmp(string1: $entry, string2: $targetName) === 0 && is_dir(filename: $basePath . '/' . $entry)) {
return $entry; // Nome com o case real
}
}
return null;
}
/**
* Renderiza uma view dentro de um layout, com suporte a módulos.
*
* Este método estático inclui o arquivo de view especificado, permitindo a
* passagem de dados para a view através do array `$data`. As variáveis
* do array `$data` são extraídas para uso dentro da view usando `extract()`.
* O método também permite especificar um layout e um módulo para a view.
*
* @param string $view O caminho para o arquivo de view, relativo ao diretório
* `views` ou `modules/{$module}/views` (ex:
* 'usuarios/listar', 'index').
* @param array $data Um array associativo contendo os dados a serem passados
* para a view. As chaves do array se tornarão variáveis
* disponíveis dentro da view.
* @param string $layout O caminho para o arquivo de layout, relativo ao
* diretório `views` (ex: 'layouts/main').
* @param string $module O nome do módulo ao qual a view pertence (opcional).
*
* @return void
*/
public static function render(string $view, array $data = [], ?string $layout = null, ?string $module = null): void {
$viewPath = VIEW_PATH . "/{$view}.php";
// Se for módulo, resolve o nome da pasta do módulo e da pasta Views
if ($module) {
$realModule = self::getRealFolderName(basePath: MODULE_PATH, targetName: $module);
if (!$realModule) {
http_response_code(response_code: 404);
die("Módulo '{$module}' não encontrado.");
}
$realViews = self::getRealFolderName(basePath: MODULE_PATH . "/{$realModule}", targetName: 'Views');
if (!$realViews) {
http_response_code(response_code: 404);
die("Pasta 'Views' do módulo '{$module}' não encontrada.");
}
$moduleViewPath = MODULE_PATH . "/{$realModule}/{$realViews}/{$view}.php";
if (file_exists(filename: $moduleViewPath)) {
$viewPath = $moduleViewPath;
}
}
if (!file_exists(filename: $viewPath)) {
http_response_code(response_code: 404);
die("View '{$view}' não encontrada.");
}
if (!empty($data)) {
extract($data, EXTR_SKIP);
}
ob_start();
require_once $viewPath;
$content = ob_get_clean();
if ($layout) {
if ($module) {
// Mesmo esquema pra layout dentro de módulo
$realModule = self::getRealFolderName(basePath: MODULE_PATH, targetName: $module);
$realViews = self::getRealFolderName(basePath: MODULE_PATH . "/{$realModule}", targetName: 'Views');
$layoutPath = MODULE_PATH . "/{$realModule}/{$realViews}/{$layout}.php";
} else {
$layoutPath = VIEW_PATH . "/{$layout}.php";
}
if (file_exists(filename: $layoutPath)) {
require_once $layoutPath;
} else {
http_response_code(response_code: 404);
die("Layout '{$layout}' não encontrado.");
}
} else {
echo $content;
}
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace AxiumPHP\Helpers;
class NavigationHelper {
private static $max_stack;
/**
* Construtor da classe que define a profundidade máxima da pilha.
*
* Este método inicializa a classe e configura a propriedade estática `self::$max_stack`
* com o valor fornecido por `$max_stack`. Essa propriedade provavelmente
* controla o número máximo de itens ou operações que podem ser armazenados em uma pilha.
* O valor padrão é 5.
*
* @param int $max_stack O número máximo de itens permitidos na pilha. O valor padrão é 5.
* @return void
*/
public function __construct(int $max_stack = 5) {
self::$max_stack = $max_stack;
}
/**
* Rastreia a navegação do usuário, mantendo um histórico das páginas visitadas
* na sessão.
*
* Este método estático obtém a URI atual da requisição. Se a URI corresponder
* a um padrão de API ou chamada AJAX, a função retorna imediatamente sem
* registrar a navegação.
*
* Se a variável de sessão 'navigation_stack' não existir, ela é inicializada
* como um array vazio.
*
* Para evitar duplicatas, verifica se a URI atual é diferente da última URI
* registrada na pilha de navegação. Se for diferente, e se a pilha atingir
* um tamanho máximo definido por `self::$max_stack`, a URI mais antiga é removida
* do início da pilha. A URI atual é então adicionada ao final da pilha.
*
* A URI atual também é armazenada na variável de sessão 'current_page', e a
* página anterior (obtida através do método `self::getPreviousPage()`) é
* armazenada em 'previous_page'.
*
* @return void
*
* @see self::getPreviousPage()
*/
public static function trackNavigation(): void {
$currentUri = $_SERVER['REQUEST_URI'] ?? '/';
// Ignora chamadas para API ou AJAX
if (preg_match(pattern: '/\/api\/|\/ajax\//i', subject: $currentUri)) {
return;
}
if (!isset($_SESSION['navigation_stack'])) {
$_SESSION['navigation_stack'] = [];
}
// Evita duplicar a última página
$last = end($_SESSION['navigation_stack']);
if ($last !== $currentUri) {
if (count(value: $_SESSION['navigation_stack']) >= self::$max_stack) {
array_shift($_SESSION['navigation_stack']);
}
$_SESSION['navigation_stack'][] = $currentUri;
}
$_SESSION['current_page'] = $currentUri;
$_SESSION['previous_page'] = self::getPreviousPage();
}
/**
* Extrai a string de query da URI da requisição.
*
* Este método estático obtém a URI completa da requisição do servidor (`$_SERVER['REQUEST_URI']`) e a divide em duas partes usando o caractere `?` como separador. A primeira parte é a URI base, e a segunda é a string de query (os parâmetros da URL).
*
* @return string|null A string de query completa (por exemplo, "param1=value1&param2=value2"), ou `null` se a URI não contiver uma string de query.
*/
public static function extractQueryString(): ?string {
$parts = explode(separator: '?', string: $_SERVER['REQUEST_URI'], limit: 2); // limite 2 garante que só divide em duas partes
$request = $parts[1] ?? null; // se não existir, define null
return ($request !== null && $request !== '') ? "?{$request}" : '';
}
/**
* Obtém a URI da página anterior visitada pelo usuário, com base na pilha
* de navegação armazenada na sessão.
*
* Este método estático acessa a variável de sessão 'navigation_stack'. Se a
* pilha contiver mais de uma URI, retorna a penúltima URI da pilha, que
* representa a página visitada imediatamente antes da atual.
*
* Se a pilha contiver apenas uma ou nenhuma URI, significa que não há uma
* página anterior no histórico de navegação da sessão, e o método retorna null.
*
* @return string|null A URI da página anterior, ou null se não houver uma.
*/
public static function getPreviousPage(): ?string {
$stack = $_SESSION['navigation_stack'] ?? [];
if (count(value: $stack) > 1) {
return $stack[count(value: $stack) - 2];
}
return null;
}
}

View File

@@ -0,0 +1,270 @@
<?php
namespace AxiumPHP\Helpers;
class RequestHelper {
/**
* Retorna os dados de entrada (GET, POST, COOKIE ou SERVER) filtrados.
*
* Permite passar filtros personalizados por campo. Se nenhum filtro for passado,
* usa o filtro padrão (`FILTER_DEFAULT`).
*
* @param int $form_type Tipo de entrada (INPUT_GET, INPUT_POST, INPUT_COOKIE ou INPUT_SERVER).
* @param array|null $filters Filtros personalizados no formato aceito por filter_input_array().
* @return array Retorna um array associativo com os dados filtrados, ou array vazio se nenhum dado for encontrado.
*/
public static function getFilteredInput(int $form_type = INPUT_GET, ?array $filters = null): array {
switch ($form_type) {
case INPUT_GET:
$form = $filters !== null ? filter_input_array(type: INPUT_GET, options: $filters) : filter_input_array(type: INPUT_GET);
break;
case INPUT_POST:
$inputData = file_get_contents(filename: 'php://input');
$data = [];
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (strpos(haystack: $contentType, needle: 'application/json') !== false) {
$data = json_decode(json: $inputData, associative: true);
if (json_last_error() !== JSON_ERROR_NONE) {
http_response_code(response_code: 500);
header(header: "Content-Type: application/json; charset=utf-8");
echo json_encode(value: [
"success" => false,
"message" => "Erro ao decodificar JSON: " . json_last_error_msg()
]);
exit;
}
if(isset($data['_method'])) {
unset($data['_method']);
}
$form = $data;
} else {
$form = $filters !== null ? filter_input_array(type: INPUT_POST, options: $filters) : filter_input_array(type: INPUT_POST);
}
break;
case INPUT_COOKIE:
$form = $filters !== null ? filter_input_array(type: INPUT_COOKIE, options: $filters) : filter_input_array(type: INPUT_COOKIE);
break;
case INPUT_SERVER:
$form = $filters !== null ? filter_input_array(type: INPUT_SERVER, options: $filters) : filter_input_array(type: INPUT_SERVER);
break;
default:
$form = $filters !== null ? filter_input_array(type: INPUT_GET, options: $filters) : filter_input_array(type: INPUT_GET);
break;
}
return $form && is_array(value: $form) ? $form : [];
}
/**
* Envia uma resposta JSON padronizada e encerra a execução do script.
*
* Este método estático é um utilitário para endpoints de API, garantindo que as respostas
* enviadas ao cliente sigam um formato consistente. Ele define o código de resposta HTTP,
* o cabeçalho `Content-Type` e um corpo JSON estruturado.
*
* O corpo da resposta inclui sempre um `status` e uma `message`. Opcionalmente, pode
* incluir um array de `data`. A validação interna garante que os parâmetros sejam
* consistentes e que a execução seja interrompida após o envio da resposta.
*
* #### Validações e Comportamento:
* - O parâmetro `status` é restrito a 'success', 'error' ou 'fail'. Se um valor inválido for passado,
* ele será padronizado para 'error'.
* - Se o parâmetro `message` estiver vazio, o código de resposta é alterado para `422` (Entidade
* Não Processável), e uma mensagem de erro padrão é definida.
* - O código de resposta HTTP é definido com `http_response_code()`.
* - O cabeçalho `Content-Type` é configurado para `application/json; charset=utf-8`.
* - O array de resposta é codificado para JSON e impresso.
* - A execução do script é finalizada com `exit`.
*
* @param int $response_code O código de status HTTP da resposta (padrão: 200).
* @param string $status O status da operação ('success', 'error' ou 'fail'). Padrão: 'success'.
* @param string $message Uma mensagem descritiva da resposta. Este campo é obrigatório e será validado.
* @param array $data Um array associativo opcional com os dados a serem retornados. Padrão: `[]`.
* @return void Este método não tem retorno, pois ele finaliza a execução do script.
*/
public static function sendJsonResponse(int $response_code = 200, string $status = 'success', string $message = '', array $output = []): void {
// Garante que o status seja válido
if (!in_array(needle: $status, haystack: ['success', 'error', 'fail'])) {
$status = 'error';
}
// Se não tiver mensagem, é erro de uso do método
if (empty($message)) {
$response_code = 422;
$status = 'error';
$message = "Mensagem obrigatória não informada.";
}
$responseArray = [
'response_code' => $response_code,
'status' => $status,
'message' => $message
];
if (isset($output['data']) && !empty($output['data'])) {
$responseArray['data'] = $output['data'];
}
if (isset($output['errors']) && !empty($output['errors'])) {
$responseArray['errors'] = $output['errors'];
}
http_response_code(response_code: $response_code);
header(header: 'Content-Type: application/json; charset=utf-8');
echo json_encode(value: $responseArray);
exit;
}
/**
* Calcula o valor de OFFSET para consultas de paginação SQL.
*
* Este método estático recebe o número da página desejada (`$page`) e o
* número de itens por página (`$limit`). Ele calcula o valor de OFFSET
* que deve ser usado em uma consulta SQL para buscar os registros corretos
* para a página especificada. O OFFSET é calculado como `($page - 1) * $limit`.
* A função `max(0, ...)` garante que o OFFSET nunca seja negativo, o que
* pode acontecer se um valor de página menor que 1 for passado.
*
* @param int $page O número da página para a qual se deseja calcular o OFFSET (a primeira página é 1).
* @param int $limit O número de itens a serem exibidos por página.
* @return int O valor de OFFSET a ser utilizado na cláusula LIMIT de uma consulta SQL.
*/
public static function getOffset(int $page, int $limit): int {
return max(0, ($page - 1) * $limit);
}
/**
* Prepara e formata os resultados de uma consulta paginada.
*
* Este método estático organiza os dados de uma resposta de banco de dados,
* juntamente com informações de paginação, em um formato padronizado.
* Ele é útil para enviar dados para a camada de visualização (frontend)
* de forma consistente.
*
* @param mixed $atuallPage O número da página atual. Pode ser um `int` ou `string`.
* @param array $dbResponse Um array contendo os resultados da consulta ao banco de dados para a página atual.
* @param string $totalResults O número total de resultados encontrados pela consulta, antes da aplicação do limite de paginação. Deve ser uma `string` (será convertido para `float`).
* @param mixed $limit O limite de resultados por página. Pode ser um `int` ou `string` (padrão é "10").
* @return array Um array associativo contendo:
* - `atuallPage`: A página atual.
* - `response`: Os dados da resposta do banco de dados para a página.
* - `totalResults`: O total de resultados, convertido para `float`.
* - `limit`: O limite de resultados por página.
*/
public static function preparePaginationResult(mixed $atuallPage, array $dbResponse, string $totalResults, mixed $limit = "10"): array {
return [
'atuallPage' => $atuallPage,
'response' => $dbResponse,
'totalResults' => floatval(value: $totalResults),
'limit' => $limit
];
}
/**
* Gera HTML para um componente de paginação.
*
* Este método estático recebe a página atual, o total de registros e um limite
* de registros por página para gerar uma navegação de paginação em HTML,
* utilizando classes do Bootstrap para estilização.
*
* Primeiro, calcula o número total de páginas. Em seguida, constrói a query
* string da URL atual, removendo os parâmetros 'page' e 'url' para evitar
* duplicações nos links de paginação.
*
* Limita o número máximo de botões de página exibidos e calcula o início e
* o fim da janela de botões a serem mostrados, ajustando essa janela para
* garantir que o número máximo de botões seja exibido, dentro dos limites
* do total de páginas.
*
* Se o total de registros for maior que o limite por página, o HTML da
* paginação é gerado, incluindo botões "Anterior" (desabilitado na primeira
* página), os números das páginas dentro da janela calculada (com a página
* atual marcada como ativa) e um botão "Próximo" (desabilitado na última
* página). Se o total de registros não for maior que o limite, uma string
* vazia é retornada.
*
* @param int $current_page O número da página atualmente visualizada.
* @param int $total_rows O número total de registros disponíveis.
* @param int $limit O número máximo de registros a serem exibidos por página (padrão: 20).
*
* @return string O HTML da navegação de paginação, estilizado com classes do Bootstrap,
* ou uma string vazia se não houver necessidade de paginação.
*/
public static function generatePaginationHtml(int $current_page, int $total_rows, int $limit = 20): string {
$current_page ??= 1;
$total_rows ??= 0;
// Calcula o total de páginas
$total_paginas = ceil(num: $total_rows / $limit);
// Construir a query string com os parâmetros atuais, exceto 'page'
$query_params = $_GET;
unset($query_params['page']); // Remove 'page' para evitar duplicação
unset($query_params['url']); // Remove 'url' para evitar duplicação
$query_string = http_build_query(data: $query_params);
// Limitar a quantidade máxima de botões a serem exibidos
$max_botoes = 10;
$inicio = max(1, $current_page - intval(value: $max_botoes / 2));
$fim = min($total_paginas, $inicio + $max_botoes - 1);
// Ajustar a janela de exibição se atingir o limite inferior ou superior
if ($fim - $inicio + 1 < $max_botoes) {
$inicio = max(1, $fim - $max_botoes + 1);
}
// Validação das paginações
if($total_rows > $limit){
// Inicia a criação do HTML da paginação
$html = "<nav aria-label='Page navigation'>";
$html .= "<ul class='pagination justify-content-center'>";
// Botão Anterior (desabilitado na primeira página)
if ($current_page > 1) {
$anterior = $current_page - 1;
$html .= "<li class='page-item'>";
$html .= "<a class='page-link' href='?{$query_string}&page={$anterior}'>Anterior</a>";
$html .= "</li>";
} else {
$html .= "<li class='page-item disabled'>";
$html .= "<a class='page-link'>Anterior</a>";
$html .= "</li>";
}
// Geração dos links de cada página dentro da janela definida
for ($i = $inicio; $i <= $fim; $i++) {
if ($i == $current_page) {
$html .= "<li class='page-item active'>";
$html .= "<a class='page-link' href='?{$query_string}&page={$i}'>{$i}</a>";
$html .= "</li>";
} else {
$html .= '<li class="page-item">';
$html .= "<a class='page-link' href='?{$query_string}&page={$i}'>{$i}</a>";
$html .= "</li>";
}
}
// Botão Próximo (desabilitado na última página)
if ($current_page < $total_paginas) {
$proxima = $current_page + 1;
$html .= "<li class='page-item'>";
$html .= "<a class='page-link' href='?{$query_string}&page={$proxima}'>Próximo</a>";
$html .= "</li>";
} else {
$html .= "<li class='page-item disabled'>";
$html .= "<a class='page-link'>Próximo</a>";
$html .= "</li>";
}
$html .= "</ul>";
$html .= "</nav>";
} else {
$html = "";
}
return $html;
}
}