feat: Complete Woles Framework v1.0 with enterprise-grade UI

- Add comprehensive error handling system with custom error pages
- Implement professional enterprise-style design with Tailwind CSS
- Create modular HMVC architecture with clean separation of concerns
- Add security features: CSRF protection, XSS filtering, Argon2ID hashing
- Include CLI tools for development workflow
- Add error reporting dashboard with system monitoring
- Implement responsive design with consistent slate color scheme
- Replace all emoji icons with professional SVG icons
- Add comprehensive test suite with PHPUnit
- Include database migrations and seeders
- Add proper exception handling with fallback pages
- Implement template engine with custom syntax support
- Add helper functions and facades for clean code
- Include proper logging and debugging capabilities
This commit is contained in:
mwpn
2025-10-11 07:08:23 +07:00
commit 0b42271bfe
90 changed files with 8315 additions and 0 deletions

210
app/Core/Bootstrap.php Normal file
View File

@@ -0,0 +1,210 @@
<?php
namespace App\Core;
use App\Core\Router;
use App\Core\Container;
use App\Core\Middleware;
use App\Core\Security;
/**
* NovaCore Framework Bootstrap
* Main application kernel
*/
class Bootstrap
{
private Container $container;
private Router $router;
private Middleware $middleware;
private Security $security;
public function __construct()
{
$this->container = new Container();
$this->router = new Router();
$this->middleware = new Middleware();
$this->security = new Security();
// Set the global container so helpers can use it
app_set_container($this->container);
$this->registerServices();
$this->loadRoutes();
$this->setupMiddleware();
}
/**
* Run the application
*/
public function run(): void
{
// Start session
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
// Initialize security
$this->security->initialize();
// Get request method and URI
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
// Run middleware pipeline
$this->middleware->run($method, $uri);
// Route the request
$route = $this->router->match($method, $uri);
if (!$route) {
$this->handleNotFound();
return;
}
// Execute controller
$this->executeController($route);
}
/**
* Register services in container
*/
private function registerServices(): void
{
$this->container->singleton('request', function () {
return new Request();
});
$this->container->singleton('response', function () {
return new Response();
});
$this->container->singleton('view', function () {
return new View();
});
$this->container->singleton('security', function () {
return $this->security;
});
}
/**
* Load routes from all modules
*/
private function loadRoutes(): void
{
$modulesPath = __DIR__ . '/../Modules';
if (is_dir($modulesPath)) {
$modules = scandir($modulesPath);
foreach ($modules as $module) {
if ($module === '.' || $module === '..') continue;
$routesFile = $modulesPath . '/' . $module . '/routes.php';
if (file_exists($routesFile)) {
// Pass router instance to routes file
$router = $this->router;
require $routesFile;
}
}
}
}
/**
* Setup default middleware stack
*/
private function setupMiddleware(): void
{
$this->middleware->add(new \App\Core\Middleware\SecurityMiddleware());
$this->middleware->add(new \App\Core\Middleware\CsrfMiddleware());
}
/**
* Execute controller method
*/
private function executeController(array $route): void
{
[$controllerClass, $method] = explode('@', $route['handler']);
// Normalize controller class to fully-qualified name
if (!str_contains($controllerClass, '\\')) {
// No backslash provided → assume default Controller in module
$controllerClass = "App\\Modules\\{$route['module']}\\Controller";
} else {
// Has backslash but may be relative like "Home\\Controller"
if (strpos($controllerClass, 'App\\') !== 0) {
$segments = explode('\\', $controllerClass);
$moduleName = $segments[0] ?? $route['module'];
$className = end($segments);
$controllerClass = "App\\Modules\\{$moduleName}\\{$className}";
}
}
if (!class_exists($controllerClass)) {
$this->handleNotFound();
return;
}
$controller = new $controllerClass();
if (!method_exists($controller, $method)) {
$this->handleNotFound();
return;
}
// Inject dependencies
$this->container->inject($controller);
// Execute method
$result = $controller->$method();
// Handle response
if ($result instanceof Response) {
$result->send();
} elseif (is_array($result) || is_object($result)) {
$response = $this->container->get('response');
$response->json($result)->send();
} else {
echo $result;
}
}
/**
* Handle 404 Not Found
*/
private function handleNotFound(): void
{
try {
$errorController = new \App\Modules\Error\Controller();
$errorController->notFound();
} catch (\Throwable $e) {
// Fallback to basic 404
http_response_code(404);
echo "<!DOCTYPE html>\n";
echo "<html>\n<head>\n";
echo "<title>404 - Page Not Found</title>\n";
echo "<script src=\"https://cdn.tailwindcss.com\"></script>\n";
echo "<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n";
echo "</head>\n<body class=\"font-sans bg-slate-50 min-h-screen\">\n";
echo "<div class=\"min-h-screen flex items-center justify-center px-4 sm:px-6 lg:px-8\">\n";
echo "<div class=\"max-w-md w-full space-y-8\">\n";
echo "<div class=\"text-center\">\n";
echo "<div class=\"mx-auto h-24 w-24 bg-slate-100 rounded-full flex items-center justify-center mb-6\">\n";
echo "<svg class=\"h-12 w-12 text-slate-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n";
echo "<path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.009-5.824-2.709M15 12a3 3 0 11-6 0 3 3 0 016 0z\"></path>\n";
echo "</svg>\n";
echo "</div>\n";
echo "<h1 class=\"text-6xl font-bold text-slate-900 mb-2\">404</h1>\n";
echo "<h2 class=\"text-2xl font-semibold text-slate-900 mb-4\">Page Not Found</h2>\n";
echo "<p class=\"text-slate-600 mb-8\">The page you are looking for could not be found.</p>\n";
echo "<div class=\"space-y-4\">\n";
echo "<a href=\"/\" class=\"w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900\">Return to Home</a>\n";
echo "<button onclick=\"history.back()\" class=\"w-full flex justify-center py-3 px-4 border border-slate-300 rounded-md shadow-sm text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500\">Go Back</button>\n";
echo "</div>\n";
echo "<div class=\"mt-8 text-sm text-slate-500\">If you believe this is an error, please contact support.</div>\n";
echo "</div>\n";
echo "</div>\n";
echo "</div>\n";
echo "</body>\n</html>\n";
}
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Core\Commands;
/**
* Woles Command Factory
* Factory for creating command instances
*/
class CommandFactory
{
/**
* Create command instance
*/
public static function create(string $command): object
{
switch ($command) {
case 'make:module':
return new MakeModuleCommand();
case 'make:controller':
return new MakeControllerCommand();
case 'make:model':
return new MakeModelCommand();
case 'serve':
return new ServeCommand();
case 'migrate':
return new MigrateCommand();
case 'migrate:rollback':
return new MigrateCommand();
case 'migrate:status':
return new MigrateCommand();
case 'seed':
return new SeedCommand();
case 'key:generate':
return new KeyGenerateCommand();
case 'help':
default:
return new HelpCommand();
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Core\Commands;
/**
* Woles Help Command
* CLI command to show help information
*/
class HelpCommand
{
private array $commands = [];
public function __construct()
{
$this->commands = [
'make:module' => 'Create a new module',
'make:controller' => 'Create a new controller',
'make:model' => 'Create a new model',
'serve' => 'Start development server',
'migrate' => 'Run database migrations',
'migrate:rollback' => 'Rollback last migration batch',
'migrate:status' => 'Show migration status',
'seed' => 'Run database seeders',
'key:generate' => 'Generate application key',
'key:generate-show' => 'Generate and show application key',
'help' => 'Show available commands'
];
}
/**
* Execute the command
*/
public function execute(): void
{
echo "Woles Framework Artisan CLI\n\n";
echo "Available commands:\n";
foreach ($this->commands as $command => $description) {
echo " " . str_pad($command, 20) . " {$description}\n";
}
echo "\nExamples:\n";
echo " php woles make:module Blog\n";
echo " php woles make:controller PostController Blog\n";
echo " php woles make:model Post Blog\n";
echo " php woles serve\n";
echo " php woles migrate\n";
echo " php woles migrate:rollback\n";
echo " php woles migrate:status\n";
echo " php woles seed\n";
echo " php woles seed UserSeeder\n";
echo " php woles key:generate\n";
echo " php woles key:generate-show\n";
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Core\Commands;
/**
* KeyGenerateCommand
* Generate and set APP_KEY in .env
*/
class KeyGenerateCommand
{
/**
* Execute the command
*/
public function execute(bool $showOnly = false): void
{
$key = bin2hex(random_bytes(32)); // 64 hex chars
if ($showOnly) {
echo $key . "\n";
return;
}
$envPath = __DIR__ . '/../../../.env';
$examplePath = __DIR__ . '/../../../env.example';
// If .env doesn't exist, try to copy from example
if (!file_exists($envPath)) {
if (file_exists($examplePath)) {
copy($examplePath, $envPath);
} else {
// Create minimal .env
file_put_contents($envPath, "APP_NAME=NovaCore Framework\nAPP_ENV=production\nAPP_DEBUG=false\n");
}
}
$content = file_get_contents($envPath) ?: '';
// Replace or append APP_KEY
if (preg_match('/^APP_KEY=.*/m', $content)) {
$content = preg_replace('/^APP_KEY=.*/m', 'APP_KEY=' . $key, $content);
} else {
$content .= (str_ends_with($content, "\n") ? '' : "\n") . 'APP_KEY=' . $key . "\n";
}
// Backup and write
@copy($envPath, $envPath . '.bak');
file_put_contents($envPath, $content);
echo "Application key set successfully.\n";
echo "APP_KEY=" . $key . "\n";
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Core\Commands;
class MakeControllerCommand
{
public function execute(string $name, string $module): void
{
if (!$name || !$module) {
echo "Error: Controller name and module are required\n";
echo "Usage: php artisan make:controller <ControllerName> <ModuleName>\n";
return;
}
$modulePath = __DIR__ . "/../../Modules/{$module}";
if (!is_dir($modulePath)) {
echo "Error: Module '{$module}' does not exist\n";
return;
}
$content = "<?php
namespace App\\Modules\\{$module};
use App\\Core\\Controller;
class {$name} extends Controller
{
public function index()
{
return \$this->view('{$module}.view.index', [
'title' => '{$module} - NovaCore Framework'
]);
}
}";
file_put_contents("{$modulePath}/{$name}.php", $content);
echo "Controller '{$name}' created in module '{$module}'!\n";
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Core\Commands;
class MakeModelCommand
{
public function execute(string $name, string $module): void
{
if (!$name || !$module) {
echo "Error: Model name and module are required\n";
echo "Usage: php artisan make:model <ModelName> <ModuleName>\n";
return;
}
$modulePath = __DIR__ . "/../../Modules/{$module}";
if (!is_dir($modulePath)) {
echo "Error: Module '{$module}' does not exist\n";
return;
}
$content = "<?php
namespace App\\Modules\\{$module};
class {$name}
{
// Add your model methods here
}";
file_put_contents("{$modulePath}/{$name}.php", $content);
echo "Model '{$name}' created in module '{$module}'!\n";
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace App\Core\Commands;
/**
* NovaCore Make Module Command
* CLI command to create new modules
*/
class MakeModuleCommand
{
private string $modulesPath;
public function __construct()
{
$this->modulesPath = __DIR__ . '/../../Modules';
}
/**
* Execute the command
*/
public function execute(string $moduleName): void
{
if (!$moduleName) {
echo "Error: Module name is required\n";
echo "Usage: php artisan make:module <ModuleName>\n";
return;
}
$modulePath = $this->modulesPath . '/' . $moduleName;
$viewPath = "{$modulePath}/view";
// Create directories
if (!is_dir($modulePath)) {
mkdir($modulePath, 0755, true);
}
if (!is_dir($viewPath)) {
mkdir($viewPath, 0755, true);
}
// Create Controller
$this->createController($moduleName, $modulePath);
// Create Model
$this->createModel($moduleName, $modulePath);
// Create Routes
$this->createRoutes($moduleName, $modulePath);
// Create View
$this->createView($moduleName, $viewPath);
echo "Module '{$moduleName}' created successfully!\n";
echo "Controller: {$modulePath}/Controller.php\n";
echo "Model: {$modulePath}/Model.php\n";
echo "Routes: {$modulePath}/routes.php\n";
echo "View: {$viewPath}/index.php\n";
}
/**
* Create controller file
*/
private function createController(string $moduleName, string $modulePath): void
{
$controllerContent = "<?php
namespace App\\Modules\\{$moduleName};
use App\\Core\\Controller;
/**
* {$moduleName} Controller
*/
class Controller extends Controller
{
public function index()
{
return \$this->view('{$moduleName}.view.index', [
'title' => '{$moduleName} - NovaCore Framework'
]);
}
}";
file_put_contents("{$modulePath}/Controller.php", $controllerContent);
}
/**
* Create model file
*/
private function createModel(string $moduleName, string $modulePath): void
{
$modelContent = "<?php
namespace App\\Modules\\{$moduleName};
/**
* {$moduleName} Model
*/
class Model
{
// Add your model methods here
}";
file_put_contents("{$modulePath}/Model.php", $modelContent);
}
/**
* Create routes file
*/
private function createRoutes(string $moduleName, string $modulePath): void
{
$routesContent = "<?php
/**
* {$moduleName} Module Routes
*/
\$router->get('/{$moduleName}', '{$moduleName}\\Controller@index');";
file_put_contents("{$modulePath}/routes.php", $routesContent);
}
/**
* Create view file
*/
private function createView(string $moduleName, string $viewPath): void
{
$viewContent = "<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>{{ \$title }}</title>
<style>
body {
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f8f9fa;
margin: 0;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #667eea;
margin-bottom: 1rem;
}
</style>
</head>
<body>
<div class=\"container\">
<h1>{{ \$title }}</h1>
<p>Welcome to the {$moduleName} module!</p>
</div>
</body>
</html>";
file_put_contents("{$viewPath}/index.php", $viewContent);
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Core\Commands;
use App\Core\Database\Migrator;
/**
* NovaCore Migrate Command
* CLI command to run database migrations
*/
class MigrateCommand
{
private Migrator $migrator;
public function __construct()
{
$this->migrator = new Migrator();
}
/**
* Execute the command
*/
public function execute(): void
{
echo "Running database migrations...\n";
$this->migrator->run();
}
/**
* Rollback migrations
*/
public function rollback(): void
{
echo "Rolling back migrations...\n";
$this->migrator->rollback();
}
/**
* Show migration status
*/
public function status(): void
{
$this->migrator->status();
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Core\Commands;
use App\Core\Database\Seeder;
/**
* NovaCore Seed Command
* CLI command to run database seeders
*/
class SeedCommand
{
private string $seedersPath;
public function __construct()
{
$this->seedersPath = __DIR__ . '/../../database/seeders';
}
/**
* Execute the command
*/
public function execute(string $seeder = null): void
{
if ($seeder) {
$this->runSeeder($seeder);
} else {
$this->runAllSeeders();
}
}
/**
* Run all seeders
*/
private function runAllSeeders(): void
{
echo "Running database seeders...\n";
$seederFiles = glob($this->seedersPath . '/*.php');
foreach ($seederFiles as $file) {
$filename = basename($file);
$className = $this->getSeederClassName($filename);
require_once $file;
if (class_exists($className)) {
$seeder = new $className();
$seeder->run();
}
}
echo "Seeding completed successfully!\n";
}
/**
* Run specific seeder
*/
private function runSeeder(string $seederName): void
{
$file = $this->seedersPath . '/' . $seederName . '.php';
if (!file_exists($file)) {
echo "Error: Seeder '{$seederName}' not found\n";
return;
}
$className = $this->getSeederClassName($seederName . '.php');
require_once $file;
if (!class_exists($className)) {
echo "Error: Seeder class '{$className}' not found\n";
return;
}
echo "Running seeder: {$seederName}\n";
$seeder = new $className();
$seeder->run();
echo "Seeder completed successfully!\n";
}
/**
* Get seeder class name
*/
private function getSeederClassName(string $filename): string
{
$name = pathinfo($filename, PATHINFO_FILENAME);
return "Database\\Seeders\\{$name}";
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Core\Commands;
/**
* NovaCore Serve Command
* CLI command to start development server
*/
class ServeCommand
{
/**
* Execute the command
*/
public function execute(): void
{
$port = getenv('APP_PORT') ?: '8000';
$host = getenv('APP_HOST') ?: 'localhost';
echo "NovaCore Framework development server starting...\n";
echo "Server running at http://{$host}:{$port}\n";
echo "Press Ctrl+C to stop the server\n\n";
exec("php -S {$host}:{$port} -t public");
}
}

128
app/Core/Container.php Normal file
View File

@@ -0,0 +1,128 @@
<?php
namespace App\Core;
/**
* NovaCore Dependency Injection Container
* Simple service container
*/
class Container
{
private array $services = [];
private array $singletons = [];
/**
* Register a service
*/
public function bind(string $name, callable $factory): void
{
$this->services[$name] = $factory;
}
/**
* Register a singleton service
*/
public function singleton(string $name, callable $factory): void
{
$this->singletons[$name] = $factory;
}
/**
* Get a service instance
*/
public function get(string $name)
{
// Check singletons first
if (isset($this->singletons[$name])) {
if (!isset($this->services[$name])) {
$this->services[$name] = $this->singletons[$name]();
}
return $this->services[$name];
}
// Check regular services
if (isset($this->services[$name])) {
if (is_callable($this->services[$name])) {
return $this->services[$name]();
}
return $this->services[$name];
}
// Try to auto-resolve
if (class_exists($name)) {
return $this->resolve($name);
}
throw new \Exception("Service '{$name}' not found in container");
}
/**
* Auto-resolve class dependencies
*/
public function resolve(string $className)
{
$reflection = new \ReflectionClass($className);
if (!$reflection->isInstantiable()) {
throw new \Exception("Class '{$className}' is not instantiable");
}
$constructor = $reflection->getConstructor();
if (!$constructor) {
return new $className();
}
$parameters = $constructor->getParameters();
$dependencies = [];
foreach ($parameters as $parameter) {
$type = $parameter->getType();
if ($type && !$type->isBuiltin()) {
$dependencies[] = $this->resolve($type->getName());
} elseif ($parameter->isDefaultValueAvailable()) {
$dependencies[] = $parameter->getDefaultValue();
} else {
throw new \Exception("Cannot resolve parameter '{$parameter->getName()}' for class '{$className}'");
}
}
return $reflection->newInstanceArgs($dependencies);
}
/**
* Inject dependencies into an object
*/
public function inject(object $object): void
{
$reflection = new \ReflectionClass($object);
$properties = $reflection->getProperties();
foreach ($properties as $property) {
if ($property->isPublic() && !$property->isInitialized($object)) {
$type = $property->getType();
if ($type && !$type->isBuiltin()) {
$property->setValue($object, $this->get($type->getName()));
}
}
}
}
/**
* Check if service exists
*/
public function has(string $name): bool
{
return isset($this->services[$name]) || isset($this->singletons[$name]);
}
/**
* Get all services
*/
public function getServices(): array
{
return array_merge($this->services, $this->singletons);
}
}

126
app/Core/Controller.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
namespace App\Core;
/**
* NovaCore Base Controller
* All controllers should extend this class
*/
abstract class Controller
{
protected Request $request;
protected Response $response;
protected View $view;
protected Security $security;
public function __construct()
{
$this->request = app('request');
$this->response = app('response');
$this->view = app('view');
$this->security = app('security');
}
/**
* Get request instance
*/
protected function request(): Request
{
return $this->request;
}
/**
* Get response instance
*/
protected function response(): Response
{
return $this->response;
}
/**
* Get underlying view engine instance
*/
protected function viewEngine(): View
{
return $this->view;
}
/**
* Get security instance
*/
protected function security(): Security
{
return $this->security;
}
/**
* Render a view
*/
protected function view(string $view, array $data = []): string
{
return $this->view->render($view, $data);
}
/**
* Return JSON response
*/
protected function json(array $data, int $status = 200): Response
{
return $this->response->json($data, $status);
}
/**
* Redirect to URL
*/
protected function redirect(string $url, int $status = 302): void
{
$this->response->redirect($url, $status);
}
/**
* Return error response
*/
protected function error(string $message, int $status = 400): Response
{
return $this->response->json(['error' => $message], $status);
}
/**
* Return success response
*/
protected function success(array $data = [], string $message = 'Success'): Response
{
return $this->response->json([
'success' => true,
'message' => $message,
'data' => $data
]);
}
/**
* Validate request data
*/
protected function validate(array $data, array $rules): array
{
$errors = [];
foreach ($rules as $field => $rule) {
$value = $data[$field] ?? null;
if (str_contains($rule, 'required') && empty($value)) {
$errors[$field] = "The {$field} field is required.";
}
if (str_contains($rule, 'email') && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
$errors[$field] = "The {$field} field must be a valid email address.";
}
if (str_contains($rule, 'min:') && strlen($value) < (int)substr($rule, 4)) {
$min = substr($rule, 4);
$errors[$field] = "The {$field} field must be at least {$min} characters.";
}
}
return $errors;
}
}

View File

@@ -0,0 +1,414 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Database Blueprint
* Schema builder for migrations
*/
class Blueprint
{
private string $table;
private array $columns = [];
private array $indexes = [];
private string $primaryKey = 'id';
private array $timestamps = [];
public function __construct(string $table)
{
$this->table = $table;
}
/**
* Add primary key
*/
public function id(string $column = 'id'): Column
{
$this->primaryKey = $column;
$column = new Column('id', 'INT AUTO_INCREMENT PRIMARY KEY');
$this->columns[] = $column;
return $column;
}
/**
* Add string column
*/
public function string(string $name, int $length = 255): Column
{
$column = new Column($name, "VARCHAR({$length})");
$this->columns[] = $column;
return $column;
}
/**
* Add text column
*/
public function text(string $name): Column
{
$column = new Column($name, 'TEXT');
$this->columns[] = $column;
return $column;
}
/**
* Add long text column
*/
public function longText(string $name): Column
{
$column = new Column($name, 'LONGTEXT');
$this->columns[] = $column;
return $column;
}
/**
* Add integer column
*/
public function integer(string $name, int $length = null): Column
{
$type = $length ? "INT({$length})" : 'INT';
$column = new Column($name, $type);
$this->columns[] = $column;
return $column;
}
/**
* Add big integer column
*/
public function bigInteger(string $name): Column
{
$column = new Column($name, 'BIGINT');
$this->columns[] = $column;
return $column;
}
/**
* Add small integer column
*/
public function smallInteger(string $name): Column
{
$column = new Column($name, 'SMALLINT');
$this->columns[] = $column;
return $column;
}
/**
* Add tiny integer column
*/
public function tinyInteger(string $name): Column
{
$column = new Column($name, 'TINYINT');
$this->columns[] = $column;
return $column;
}
/**
* Add boolean column
*/
public function boolean(string $name): Column
{
$column = new Column($name, 'BOOLEAN');
$this->columns[] = $column;
return $column;
}
/**
* Add decimal column
*/
public function decimal(string $name, int $precision = 8, int $scale = 2): Column
{
$column = new Column($name, "DECIMAL({$precision}, {$scale})");
$this->columns[] = $column;
return $column;
}
/**
* Add float column
*/
public function float(string $name, int $precision = 8, int $scale = 2): Column
{
$column = new Column($name, "FLOAT({$precision}, {$scale})");
$this->columns[] = $column;
return $column;
}
/**
* Add double column
*/
public function double(string $name, int $precision = 8, int $scale = 2): Column
{
$column = new Column($name, "DOUBLE({$precision}, {$scale})");
$this->columns[] = $column;
return $column;
}
/**
* Add date column
*/
public function date(string $name): Column
{
$column = new Column($name, 'DATE');
$this->columns[] = $column;
return $column;
}
/**
* Add datetime column
*/
public function datetime(string $name): Column
{
$column = new Column($name, 'DATETIME');
$this->columns[] = $column;
return $column;
}
/**
* Add timestamp column
*/
public function timestamp(string $name): Column
{
$column = new Column($name, 'TIMESTAMP');
$this->columns[] = $column;
return $column;
}
/**
* Add timestamps
*/
public function timestamps(): void
{
$this->timestamp('created_at')->nullable();
$this->timestamp('updated_at')->nullable();
}
/**
* Add soft deletes
*/
public function softDeletes(): void
{
$this->timestamp('deleted_at')->nullable();
}
/**
* Add index
*/
public function index(array $columns, string $name = null): void
{
if (!$name) {
$name = $this->table . '_' . implode('_', $columns) . '_index';
}
$this->indexes[] = [
'name' => $name,
'columns' => $columns,
'type' => 'INDEX'
];
}
/**
* Add unique index
*/
public function unique(array $columns, string $name = null): void
{
if (!$name) {
$name = $this->table . '_' . implode('_', $columns) . '_unique';
}
$this->indexes[] = [
'name' => $name,
'columns' => $columns,
'type' => 'UNIQUE'
];
}
/**
* Add foreign key
*/
public function foreign(string $column): ForeignKey
{
$foreignKey = new ForeignKey($column);
$this->columns[] = $foreignKey;
return $foreignKey;
}
/**
* Generate SQL
*/
public function toSql(): string
{
$sql = "CREATE TABLE `{$this->table}` (";
$columnDefinitions = [];
foreach ($this->columns as $column) {
$columnDefinitions[] = $column->toSql();
}
$sql .= implode(', ', $columnDefinitions);
$sql .= ")";
return $sql;
}
}
/**
* Column definition
*/
class Column
{
private string $name;
private string $type;
private bool $nullable = false;
private $default = null;
private bool $autoIncrement = false;
private bool $primary = false;
private bool $unique = false;
public function __construct(string $name, string $type)
{
$this->name = $name;
$this->type = $type;
}
/**
* Set nullable
*/
public function nullable(): self
{
$this->nullable = true;
return $this;
}
/**
* Set default value
*/
public function default($value): self
{
$this->default = $value;
return $this;
}
/**
* Set auto increment
*/
public function autoIncrement(): self
{
$this->autoIncrement = true;
return $this;
}
/**
* Set primary key
*/
public function primary(): self
{
$this->primary = true;
return $this;
}
/**
* Set unique
*/
public function unique(): self
{
$this->unique = true;
return $this;
}
/**
* Generate SQL
*/
public function toSql(): string
{
$sql = "`{$this->name}` {$this->type}";
if ($this->autoIncrement) {
$sql .= " AUTO_INCREMENT";
}
if (!$this->nullable) {
$sql .= " NOT NULL";
}
if ($this->default !== null) {
$default = is_string($this->default) ? "'{$this->default}'" : $this->default;
$sql .= " DEFAULT {$default}";
}
if ($this->unique) {
$sql .= " UNIQUE";
}
if ($this->primary) {
$sql .= " PRIMARY KEY";
}
return $sql;
}
}
/**
* Foreign key definition
*/
class ForeignKey
{
private string $column;
private string $references;
private string $on;
private string $onDelete = 'RESTRICT';
private string $onUpdate = 'RESTRICT';
public function __construct(string $column)
{
$this->column = $column;
}
/**
* Set references
*/
public function references(string $column): self
{
$this->references = $column;
return $this;
}
/**
* Set on table
*/
public function on(string $table): self
{
$this->on = $table;
return $this;
}
/**
* Set on delete action
*/
public function onDelete(string $action): self
{
$this->onDelete = $action;
return $this;
}
/**
* Set on update action
*/
public function onUpdate(string $action): self
{
$this->onUpdate = $action;
return $this;
}
/**
* Generate SQL
*/
public function toSql(): string
{
$sql = "`{$this->column}` INT";
if ($this->references && $this->on) {
$sql .= ", FOREIGN KEY (`{$this->column}`) REFERENCES `{$this->on}` (`{$this->references}`)";
$sql .= " ON DELETE {$this->onDelete} ON UPDATE {$this->onUpdate}";
}
return $sql;
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace App\Core\Database;
use PDO;
use PDOException;
/**
* NovaCore Database Connection
* PDO database wrapper
*/
class Connection
{
private PDO $pdo;
private array $config;
public function __construct(array $config)
{
$this->config = $config;
$this->connect();
}
/**
* Establish database connection
*/
private function connect(): void
{
try {
$dsn = $this->buildDsn();
if ($this->config['driver'] === 'sqlite') {
$this->pdo = new PDO($dsn);
} else {
$this->pdo = new PDO(
$dsn,
$this->config['username'],
$this->config['password'],
$this->config['options'] ?? []
);
}
} catch (PDOException $e) {
throw new \Exception("Database connection failed: " . $e->getMessage());
}
}
/**
* Build DSN string
*/
private function buildDsn(): string
{
$driver = $this->config['driver'];
switch ($driver) {
case 'mysql':
$host = $this->config['host'];
$port = $this->config['port'] ?? null;
$database = $this->config['database'];
$charset = $this->config['charset'] ?? 'utf8';
return "mysql:host={$host};port={$port};dbname={$database};charset={$charset}";
case 'pgsql':
$host = $this->config['host'];
$port = $this->config['port'] ?? null;
$database = $this->config['database'];
return "pgsql:host={$host};port={$port};dbname={$database}";
case 'sqlite':
$database = $this->config['database'];
return "sqlite:{$database}";
default:
throw new \Exception("Unsupported database driver: {$driver}");
}
}
/**
* Get PDO instance
*/
public function getPdo(): PDO
{
return $this->pdo;
}
/**
* Execute query
*/
public function query(string $sql, array $params = []): \PDOStatement
{
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
/**
* Execute query and return all results
*/
public function fetchAll(string $sql, array $params = []): array
{
$stmt = $this->query($sql, $params);
return $stmt->fetchAll();
}
/**
* Execute query and return single result
*/
public function fetch(string $sql, array $params = []): ?array
{
$stmt = $this->query($sql, $params);
$result = $stmt->fetch();
return $result ?: null;
}
/**
* Execute query and return single value
*/
public function fetchColumn(string $sql, array $params = [])
{
$stmt = $this->query($sql, $params);
return $stmt->fetchColumn();
}
/**
* Execute query and return affected rows
*/
public function execute(string $sql, array $params = []): int
{
$stmt = $this->query($sql, $params);
return $stmt->rowCount();
}
/**
* Begin transaction
*/
public function beginTransaction(): bool
{
return $this->pdo->beginTransaction();
}
/**
* Commit transaction
*/
public function commit(): bool
{
return $this->pdo->commit();
}
/**
* Rollback transaction
*/
public function rollback(): bool
{
return $this->pdo->rollBack();
}
/**
* Get last insert ID
*/
public function lastInsertId(): string
{
return $this->pdo->lastInsertId();
}
/**
* Check if connected
*/
public function isConnected(): bool
{
try {
$this->pdo->query('SELECT 1');
return true;
} catch (PDOException $e) {
return false;
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Database Migration
* Base migration class
*/
abstract class Migration
{
protected Connection $connection;
public function __construct()
{
$this->connection = $this->getConnection();
}
/**
* Get database connection
*/
protected function getConnection(): Connection
{
$config = include __DIR__ . '/../../Config/database.php';
$connectionConfig = $config['connections'][$config['default']];
return new Connection($connectionConfig);
}
/**
* Run the migration
*/
abstract public function up(): void;
/**
* Reverse the migration
*/
abstract public function down(): void;
/**
* Create table
*/
protected function createTable(string $table, callable $callback): void
{
$blueprint = new Blueprint($table);
$callback($blueprint);
$sql = $blueprint->toSql();
$this->connection->execute($sql);
}
/**
* Drop table
*/
protected function dropTable(string $table): void
{
$sql = "DROP TABLE IF EXISTS `{$table}`";
$this->connection->execute($sql);
}
/**
* Add column
*/
protected function addColumn(string $table, string $column, string $type): void
{
$sql = "ALTER TABLE `{$table}` ADD COLUMN `{$column}` {$type}";
$this->connection->execute($sql);
}
/**
* Drop column
*/
protected function dropColumn(string $table, string $column): void
{
$sql = "ALTER TABLE `{$table}` DROP COLUMN `{$column}`";
$this->connection->execute($sql);
}
/**
* Rename column
*/
protected function renameColumn(string $table, string $from, string $to): void
{
$sql = "ALTER TABLE `{$table}` RENAME COLUMN `{$from}` TO `{$to}`";
$this->connection->execute($sql);
}
/**
* Add index
*/
protected function addIndex(string $table, string $index, array $columns): void
{
$columnsStr = implode(', ', array_map(fn($col) => "`{$col}`", $columns));
$sql = "CREATE INDEX `{$index}` ON `{$table}` ({$columnsStr})";
$this->connection->execute($sql);
}
/**
* Drop index
*/
protected function dropIndex(string $table, string $index): void
{
$sql = "DROP INDEX `{$index}` ON `{$table}`";
$this->connection->execute($sql);
}
}

View File

@@ -0,0 +1,249 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Database Migrator
* Handle database migrations
*/
class Migrator
{
private Connection $connection;
private string $migrationsPath;
public function __construct()
{
$this->connection = $this->getConnection();
$this->migrationsPath = __DIR__ . '/../../database/migrations';
}
/**
* Get database connection
*/
protected function getConnection(): Connection
{
$config = include __DIR__ . '/../../Config/database.php';
$connectionConfig = $config['connections'][$config['default']];
return new Connection($connectionConfig);
}
/**
* Run all pending migrations
*/
public function run(): void
{
$this->createMigrationsTable();
$migrations = $this->getPendingMigrations();
foreach ($migrations as $migration) {
$this->runMigration($migration);
}
echo "Migrations completed successfully!\n";
}
/**
* Create migrations table
*/
private function createMigrationsTable(): void
{
$sql = "CREATE TABLE IF NOT EXISTS migrations (
id INT AUTO_INCREMENT PRIMARY KEY,
migration VARCHAR(255) NOT NULL,
batch INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)";
$this->connection->execute($sql);
}
/**
* Get pending migrations
*/
private function getPendingMigrations(): array
{
$migrationFiles = glob($this->migrationsPath . '/*.php');
$migratedFiles = $this->getMigratedFiles();
$pending = [];
foreach ($migrationFiles as $file) {
$filename = basename($file);
if (!in_array($filename, $migratedFiles)) {
$pending[] = $file;
}
}
sort($pending);
return $pending;
}
/**
* Get already migrated files
*/
private function getMigratedFiles(): array
{
$sql = "SELECT migration FROM migrations ORDER BY id";
$results = $this->connection->fetchAll($sql);
return array_column($results, 'migration');
}
/**
* Run single migration
*/
private function runMigration(string $file): void
{
$filename = basename($file);
$className = $this->getMigrationClassName($filename);
require_once $file;
if (!class_exists($className)) {
throw new \Exception("Migration class {$className} not found in {$file}");
}
$migration = new $className();
$migration->up();
// Record migration
$this->recordMigration($filename);
echo "{$filename}\n";
}
/**
* Get migration class name
*/
private function getMigrationClassName(string $filename): string
{
$name = pathinfo($filename, PATHINFO_FILENAME);
$parts = explode('_', $name);
// Remove timestamp
array_shift($parts);
// Convert to PascalCase
$className = '';
foreach ($parts as $part) {
$className .= ucfirst($part);
}
return $className;
}
/**
* Record migration
*/
private function recordMigration(string $filename): void
{
$batch = $this->getNextBatchNumber();
$sql = "INSERT INTO migrations (migration, batch) VALUES (?, ?)";
$this->connection->execute($sql, [$filename, $batch]);
}
/**
* Get next batch number
*/
private function getNextBatchNumber(): int
{
$sql = "SELECT MAX(batch) as max_batch FROM migrations";
$result = $this->connection->fetch($sql);
return ($result['max_batch'] ?? 0) + 1;
}
/**
* Rollback last batch
*/
public function rollback(): void
{
$lastBatch = $this->getLastBatchNumber();
if (!$lastBatch) {
echo "No migrations to rollback.\n";
return;
}
$migrations = $this->getMigrationsByBatch($lastBatch);
foreach (array_reverse($migrations) as $migration) {
$this->rollbackMigration($migration);
}
echo "Rollback completed successfully!\n";
}
/**
* Get last batch number
*/
private function getLastBatchNumber(): ?int
{
$sql = "SELECT MAX(batch) as max_batch FROM migrations";
$result = $this->connection->fetch($sql);
return $result['max_batch'] ?? null;
}
/**
* Get migrations by batch
*/
private function getMigrationsByBatch(int $batch): array
{
$sql = "SELECT migration FROM migrations WHERE batch = ? ORDER BY id DESC";
$results = $this->connection->fetchAll($sql, [$batch]);
return array_column($results, 'migration');
}
/**
* Rollback single migration
*/
private function rollbackMigration(string $filename): void
{
$file = $this->migrationsPath . '/' . $filename;
if (!file_exists($file)) {
echo "Warning: Migration file {$filename} not found.\n";
return;
}
$className = $this->getMigrationClassName($filename);
require_once $file;
if (!class_exists($className)) {
echo "Warning: Migration class {$className} not found.\n";
return;
}
$migration = new $className();
$migration->down();
// Remove migration record
$sql = "DELETE FROM migrations WHERE migration = ?";
$this->connection->execute($sql, [$filename]);
echo "✓ Rolled back {$filename}\n";
}
/**
* Get migration status
*/
public function status(): void
{
$migrationFiles = glob($this->migrationsPath . '/*.php');
$migratedFiles = $this->getMigratedFiles();
echo "Migration Status:\n";
echo "================\n";
foreach ($migrationFiles as $file) {
$filename = basename($file);
$status = in_array($filename, $migratedFiles) ? '✓' : '✗';
echo "{$status} {$filename}\n";
}
}
}

256
app/Core/Database/Model.php Normal file
View File

@@ -0,0 +1,256 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Base Model
* Base model class for database operations
*/
abstract class Model
{
protected Connection $connection;
protected string $table;
protected string $primaryKey = 'id';
protected array $fillable = [];
protected array $guarded = [];
protected array $attributes = [];
protected bool $timestamps = true;
protected string $createdAt = 'created_at';
protected string $updatedAt = 'updated_at';
public function __construct(array $attributes = [])
{
$this->attributes = $attributes;
$this->connection = $this->getConnection();
}
/**
* Get database connection
*/
protected function getConnection(): Connection
{
$config = include __DIR__ . '/../../Config/database.php';
$connectionConfig = $config['connections'][$config['default']];
return new Connection($connectionConfig);
}
/**
* Get query builder instance
*/
protected function newQuery(): QueryBuilder
{
return new QueryBuilder($this->connection, $this->table);
}
/**
* Find record by ID
*/
public static function find(int $id): ?self
{
$instance = new static();
$result = $instance->newQuery()->where($instance->primaryKey, $id)->first();
if (!$result) {
return null;
}
return new static($result);
}
/**
* Find all records
*/
public static function all(): array
{
$instance = new static();
$results = $instance->newQuery()->get();
return array_map(function ($attributes) {
return new static($attributes);
}, $results);
}
/**
* Create new record
*/
public static function create(array $attributes): self
{
$instance = new static();
$instance->fill($attributes);
$instance->save();
return $instance;
}
/**
* Fill model attributes
*/
public function fill(array $attributes): self
{
foreach ($attributes as $key => $value) {
if ($this->isFillable($key)) {
$this->attributes[$key] = $value;
}
}
return $this;
}
/**
* Check if attribute is fillable
*/
protected function isFillable(string $key): bool
{
if (in_array($key, $this->guarded)) {
return false;
}
if (empty($this->fillable)) {
return true;
}
return in_array($key, $this->fillable);
}
/**
* Save model to database
*/
public function save(): bool
{
if ($this->exists()) {
return $this->update();
} else {
return $this->insert();
}
}
/**
* Check if model exists in database
*/
public function exists(): bool
{
return isset($this->attributes[$this->primaryKey]) && $this->attributes[$this->primaryKey] !== null;
}
/**
* Insert new record
*/
protected function insert(): bool
{
$attributes = $this->attributes;
if ($this->timestamps) {
$now = date('Y-m-d H:i:s');
$attributes[$this->createdAt] = $now;
$attributes[$this->updatedAt] = $now;
}
$result = $this->newQuery()->insert($attributes);
if ($result) {
$this->attributes[$this->primaryKey] = $this->connection->lastInsertId();
}
return $result > 0;
}
/**
* Update existing record
*/
protected function update(): bool
{
$attributes = $this->attributes;
unset($attributes[$this->primaryKey]);
if ($this->timestamps) {
$attributes[$this->updatedAt] = date('Y-m-d H:i:s');
}
$result = $this->newQuery()
->where($this->primaryKey, $this->attributes[$this->primaryKey])
->update($attributes);
return $result > 0;
}
/**
* Delete record
*/
public function delete(): bool
{
if (!$this->exists()) {
return false;
}
$result = $this->newQuery()
->where($this->primaryKey, $this->attributes[$this->primaryKey])
->delete();
return $result > 0;
}
/**
* Get attribute value
*/
public function __get(string $key)
{
return $this->attributes[$key] ?? null;
}
/**
* Set attribute value
*/
public function __set(string $key, $value): void
{
$this->attributes[$key] = $value;
}
/**
* Check if attribute exists
*/
public function __isset(string $key): bool
{
return isset($this->attributes[$key]);
}
/**
* Convert model to array
*/
public function toArray(): array
{
return $this->attributes;
}
/**
* Convert model to JSON
*/
public function toJson(): string
{
return json_encode($this->attributes);
}
/**
* Get table name
*/
public function getTable(): string
{
return $this->table;
}
/**
* Get primary key
*/
public function getKeyName(): string
{
return $this->primaryKey;
}
/**
* Get primary key value
*/
public function getKey()
{
return $this->attributes[$this->primaryKey] ?? null;
}
}

View File

@@ -0,0 +1,443 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Query Builder
* Simple query builder for database operations
*/
class QueryBuilder
{
private Connection $connection;
private string $table;
private array $select = ['*'];
private array $where = [];
private array $orderBy = [];
private array $groupBy = [];
private array $having = [];
private ?int $limit = null;
private ?int $offset = null;
private array $joins = [];
public function __construct(Connection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
/**
* Select columns
*/
public function select(array $columns): self
{
$this->select = $columns;
return $this;
}
/**
* Add where condition
*/
public function where(string $column, $operator, $value = null): self
{
if ($value === null) {
$value = $operator;
$operator = '=';
}
$this->where[] = [
'column' => $column,
'operator' => $operator,
'value' => $value,
'boolean' => 'AND'
];
return $this;
}
/**
* Add OR where condition
*/
public function orWhere(string $column, $operator, $value = null): self
{
if ($value === null) {
$value = $operator;
$operator = '=';
}
$this->where[] = [
'column' => $column,
'operator' => $operator,
'value' => $value,
'boolean' => 'OR'
];
return $this;
}
/**
* Add where in condition
*/
public function whereIn(string $column, array $values): self
{
$this->where[] = [
'column' => $column,
'operator' => 'IN',
'value' => $values,
'boolean' => 'AND'
];
return $this;
}
/**
* Add where not in condition
*/
public function whereNotIn(string $column, array $values): self
{
$this->where[] = [
'column' => $column,
'operator' => 'NOT IN',
'value' => $values,
'boolean' => 'AND'
];
return $this;
}
/**
* Add where null condition
*/
public function whereNull(string $column): self
{
$this->where[] = [
'column' => $column,
'operator' => 'IS NULL',
'value' => null,
'boolean' => 'AND'
];
return $this;
}
/**
* Add where not null condition
*/
public function whereNotNull(string $column): self
{
$this->where[] = [
'column' => $column,
'operator' => 'IS NOT NULL',
'value' => null,
'boolean' => 'AND'
];
return $this;
}
/**
* Add order by clause
*/
public function orderBy(string $column, string $direction = 'ASC'): self
{
$this->orderBy[] = [
'column' => $column,
'direction' => strtoupper($direction)
];
return $this;
}
/**
* Add group by clause
*/
public function groupBy(string $column): self
{
$this->groupBy[] = $column;
return $this;
}
/**
* Add having clause
*/
public function having(string $column, $operator, $value): self
{
$this->having[] = [
'column' => $column,
'operator' => $operator,
'value' => $value,
'boolean' => 'AND'
];
return $this;
}
/**
* Add limit clause
*/
public function limit(int $limit): self
{
$this->limit = $limit;
return $this;
}
/**
* Add offset clause
*/
public function offset(int $offset): self
{
$this->offset = $offset;
return $this;
}
/**
* Add join clause
*/
public function join(string $table, string $first, string $operator, string $second, string $type = 'INNER'): self
{
$this->joins[] = [
'table' => $table,
'first' => $first,
'operator' => $operator,
'second' => $second,
'type' => $type
];
return $this;
}
/**
* Add left join clause
*/
public function leftJoin(string $table, string $first, string $operator, string $second): self
{
return $this->join($table, $first, $operator, $second, 'LEFT');
}
/**
* Add right join clause
*/
public function rightJoin(string $table, string $first, string $operator, string $second): self
{
return $this->join($table, $first, $operator, $second, 'RIGHT');
}
/**
* Get all results
*/
public function get(): array
{
$sql = $this->toSql();
$params = $this->getBindings();
return $this->connection->fetchAll($sql, $params);
}
/**
* Get first result
*/
public function first(): ?array
{
$this->limit(1);
$sql = $this->toSql();
$params = $this->getBindings();
return $this->connection->fetch($sql, $params);
}
/**
* Get count
*/
public function count(): int
{
$this->select = ['COUNT(*) as count'];
$result = $this->first();
return (int) $result['count'];
}
/**
* Insert data
*/
public function insert(array $data): int
{
$columns = array_keys($data);
$values = array_values($data);
$placeholders = array_fill(0, count($values), '?');
$sql = "INSERT INTO {$this->table} (" . implode(', ', $columns) . ") VALUES (" . implode(', ', $placeholders) . ")";
return $this->connection->execute($sql, $values);
}
/**
* Update data
*/
public function update(array $data): int
{
$columns = array_keys($data);
$values = array_values($data);
$set = [];
foreach ($columns as $column) {
$set[] = "{$column} = ?";
}
$sql = "UPDATE {$this->table} SET " . implode(', ', $set);
$params = $values;
if (!empty($this->where)) {
$sql .= " WHERE " . $this->buildWhereClause();
$params = array_merge($params, $this->getWhereBindings());
}
return $this->connection->execute($sql, $params);
}
/**
* Delete records
*/
public function delete(): int
{
$sql = "DELETE FROM {$this->table}";
$params = [];
if (!empty($this->where)) {
$sql .= " WHERE " . $this->buildWhereClause();
$params = $this->getWhereBindings();
}
return $this->connection->execute($sql, $params);
}
/**
* Build SQL query
*/
private function toSql(): string
{
$sql = "SELECT " . implode(', ', $this->select) . " FROM {$this->table}";
// Add joins
foreach ($this->joins as $join) {
$sql .= " {$join['type']} JOIN {$join['table']} ON {$join['first']} {$join['operator']} {$join['second']}";
}
// Add where clause
if (!empty($this->where)) {
$sql .= " WHERE " . $this->buildWhereClause();
}
// Add group by clause
if (!empty($this->groupBy)) {
$sql .= " GROUP BY " . implode(', ', $this->groupBy);
}
// Add having clause
if (!empty($this->having)) {
$sql .= " HAVING " . $this->buildHavingClause();
}
// Add order by clause
if (!empty($this->orderBy)) {
$orderBy = [];
foreach ($this->orderBy as $order) {
$orderBy[] = "{$order['column']} {$order['direction']}";
}
$sql .= " ORDER BY " . implode(', ', $orderBy);
}
// Add limit clause
if ($this->limit !== null) {
$sql .= " LIMIT {$this->limit}";
}
// Add offset clause
if ($this->offset !== null) {
$sql .= " OFFSET {$this->offset}";
}
return $sql;
}
/**
* Build where clause
*/
private function buildWhereClause(): string
{
$clauses = [];
foreach ($this->where as $index => $condition) {
$clause = '';
if ($index > 0) {
$clause .= " {$condition['boolean']} ";
}
if ($condition['operator'] === 'IN' || $condition['operator'] === 'NOT IN') {
$placeholders = array_fill(0, count($condition['value']), '?');
$clause .= "{$condition['column']} {$condition['operator']} (" . implode(', ', $placeholders) . ")";
} else {
$clause .= "{$condition['column']} {$condition['operator']} ?";
}
$clauses[] = $clause;
}
return implode('', $clauses);
}
/**
* Build having clause
*/
private function buildHavingClause(): string
{
$clauses = [];
foreach ($this->having as $index => $condition) {
$clause = '';
if ($index > 0) {
$clause .= " {$condition['boolean']} ";
}
$clause .= "{$condition['column']} {$condition['operator']} ?";
$clauses[] = $clause;
}
return implode('', $clauses);
}
/**
* Get all bindings
*/
private function getBindings(): array
{
$bindings = [];
// Add where bindings
$bindings = array_merge($bindings, $this->getWhereBindings());
// Add having bindings
foreach ($this->having as $condition) {
$bindings[] = $condition['value'];
}
return $bindings;
}
/**
* Get where bindings
*/
private function getWhereBindings(): array
{
$bindings = [];
foreach ($this->where as $condition) {
if ($condition['operator'] === 'IN' || $condition['operator'] === 'NOT IN') {
$bindings = array_merge($bindings, $condition['value']);
} else {
$bindings[] = $condition['value'];
}
}
return $bindings;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Core\Database;
/**
* NovaCore Database Seeder
* Base seeder class
*/
abstract class Seeder
{
protected Connection $connection;
public function __construct()
{
$this->connection = $this->getConnection();
}
/**
* Get database connection
*/
protected function getConnection(): Connection
{
$config = include __DIR__ . '/../../Config/database.php';
$connectionConfig = $config['connections'][$config['default']];
return new Connection($connectionConfig);
}
/**
* Run the seeder
*/
abstract public function run(): void;
/**
* Call another seeder
*/
protected function call(string $seeder): void
{
$seederClass = "Database\\Seeders\\{$seeder}";
if (!class_exists($seederClass)) {
throw new \Exception("Seeder class {$seederClass} not found");
}
$seederInstance = new $seederClass();
$seederInstance->run();
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Core\Exceptions;
/**
* 419 CSRF Token Mismatch Exception
*/
class CsrfMismatchException extends \Exception
{
public function __construct(string $message = "CSRF token mismatch", int $code = 419, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Core\Exceptions;
/**
* 403 Forbidden Exception
*/
class ForbiddenException extends \Exception
{
public function __construct(string $message = "Access forbidden", int $code = 403, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace App\Core\Exceptions;
use App\Modules\Error\Controller as ErrorController;
/**
* NovaCore Exception Handler
* Global exception handling with modern error pages
*/
class Handler
{
/**
* Handle exception
*/
public function handle(\Throwable $e): void
{
// Log the exception
$this->logException($e);
// Show error page
$this->renderException($e);
}
/**
* Log exception
*/
private function logException(\Throwable $e): void
{
$logFile = storage_path('logs/error.log');
$timestamp = date('Y-m-d H:i:s');
$message = "[{$timestamp}] " . get_class($e) . ": {$e->getMessage()}\n";
$message .= "File: {$e->getFile()}:{$e->getLine()}\n";
$message .= "Stack trace:\n{$e->getTraceAsString()}\n\n";
file_put_contents($logFile, $message, FILE_APPEND | LOCK_EX);
}
/**
* Render exception
*/
private function renderException(\Throwable $e): void
{
try {
$errorController = new ErrorController();
// Determine error type and render appropriate page
if ($e instanceof \App\Core\Exceptions\NotFoundException) {
$errorController->notFound();
} elseif ($e instanceof \App\Core\Exceptions\ForbiddenException) {
$errorController->forbidden();
} elseif ($e instanceof \App\Core\Exceptions\UnauthorizedException) {
$errorController->unauthorized();
} elseif ($e instanceof \App\Core\Exceptions\CsrfMismatchException) {
$errorController->csrfMismatch();
} else {
$errorController->serverError($e);
}
} catch (\Throwable $renderException) {
// Fallback to basic error page if error rendering fails
$this->renderFallbackException($e);
}
}
/**
* Render fallback exception (when error page rendering fails)
*/
private function renderFallbackException(\Throwable $e): void
{
http_response_code(500);
if (is_development()) {
$this->renderDevelopmentException($e);
} else {
$this->renderProductionException();
}
}
/**
* Render development exception
*/
private function renderDevelopmentException(\Throwable $e): void
{
http_response_code(500);
echo "<!DOCTYPE html>\n";
echo "<html>\n<head>\n";
echo "<title>Woles Framework Error</title>\n";
echo "<script src=\"https://cdn.tailwindcss.com\"></script>\n";
echo "<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n";
echo "</head>\n<body class=\"font-sans bg-slate-50 min-h-screen p-8\">\n";
echo "<div class=\"max-w-4xl mx-auto\">\n";
echo "<div class=\"bg-white rounded-xl shadow-sm border border-red-200 p-8\">\n";
echo "<div class=\"flex items-center mb-6\">\n";
echo "<div class=\"w-12 h-12 bg-red-100 rounded-lg flex items-center justify-center mr-4\">\n";
echo "<svg class=\"h-6 w-6 text-red-600\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n";
echo "<path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z\"></path>\n";
echo "</svg>\n";
echo "</div>\n";
echo "<h1 class=\"text-2xl font-bold text-slate-900\">" . get_class($e) . "</h1>\n";
echo "</div>\n";
echo "<div class=\"bg-red-50 border border-red-200 rounded-lg p-4 mb-6\">\n";
echo "<p class=\"text-red-800 font-medium\">" . htmlspecialchars($e->getMessage()) . "</p>\n";
echo "</div>\n";
echo "<div class=\"bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4\">\n";
echo "<p class=\"text-sm text-slate-600\"><strong>File:</strong> " . htmlspecialchars($e->getFile()) . ":" . $e->getLine() . "</p>\n";
echo "</div>\n";
echo "<div class=\"bg-slate-900 text-green-400 p-4 rounded-lg font-mono text-xs overflow-x-auto\">\n";
echo "<pre>" . htmlspecialchars($e->getTraceAsString()) . "</pre>\n";
echo "</div>\n";
echo "<div class=\"mt-6 text-center\">\n";
echo "<a href=\"/\" class=\"bg-slate-900 hover:bg-slate-800 text-white px-6 py-3 rounded-md font-medium transition-colors\">Go Home</a>\n";
echo "</div>\n";
echo "</div>\n";
echo "</div>\n";
echo "</body>\n</html>\n";
}
/**
* Render production exception
*/
private function renderProductionException(): void
{
http_response_code(500);
echo "<!DOCTYPE html>\n";
echo "<html>\n<head>\n";
echo "<title>Server Error - Woles Framework</title>\n";
echo "<script src=\"https://cdn.tailwindcss.com\"></script>\n";
echo "<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\" rel=\"stylesheet\">\n";
echo "</head>\n<body class=\"font-sans bg-slate-50 min-h-screen\">\n";
echo "<div class=\"min-h-screen flex items-center justify-center px-4 sm:px-6 lg:px-8\">\n";
echo "<div class=\"max-w-md w-full space-y-8\">\n";
echo "<div class=\"text-center\">\n";
echo "<div class=\"mx-auto h-24 w-24 bg-slate-100 rounded-full flex items-center justify-center mb-6\">\n";
echo "<svg class=\"h-12 w-12 text-slate-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n";
echo "<path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z\"></path>\n";
echo "</svg>\n";
echo "</div>\n";
echo "<h1 class=\"text-6xl font-bold text-slate-900 mb-2\">500</h1>\n";
echo "<h2 class=\"text-2xl font-semibold text-slate-900 mb-4\">Server Error</h2>\n";
echo "<p class=\"text-slate-600 mb-8\">Something went wrong on our end.</p>\n";
echo "<div class=\"space-y-4\">\n";
echo "<a href=\"/\" class=\"w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-slate-900 hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-900\">Return to Home</a>\n";
echo "<button onclick=\"location.reload()\" class=\"w-full flex justify-center py-3 px-4 border border-slate-300 rounded-md shadow-sm text-sm font-medium text-slate-700 bg-white hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500\">Try Again</button>\n";
echo "</div>\n";
echo "<div class=\"mt-8 text-sm text-slate-500\">We're working to fix this issue. Please try again later.</div>\n";
echo "</div>\n";
echo "</div>\n";
echo "</div>\n";
echo "</body>\n</html>\n";
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Core\Exceptions;
/**
* 404 Not Found Exception
*/
class NotFoundException extends \Exception
{
public function __construct(string $message = "Page not found", int $code = 404, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Core\Exceptions;
/**
* 401 Unauthorized Exception
*/
class UnauthorizedException extends \Exception
{
public function __construct(string $message = "Unauthorized access", int $code = 401, ?\Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

34
app/Core/Facades/App.php Normal file
View File

@@ -0,0 +1,34 @@
<?php
namespace App\Core\Facades;
/**
* NovaCore App Facade
* Static access to application services
*/
class App
{
/**
* Get service from container
*/
public static function get(string $name)
{
return app($name);
}
/**
* Check if service exists
*/
public static function has(string $name): bool
{
return app()->has($name);
}
/**
* Get all services
*/
public static function all(): array
{
return app()->getServices();
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Core\Facades;
/**
* NovaCore Request Facade
* Static access to request services
*/
class Request
{
/**
* Get request instance
*/
public static function instance(): \App\Core\Request
{
return app('request');
}
/**
* Get all input data
*/
public static function all(): array
{
return self::instance()->all();
}
/**
* Get input value by key
*/
public static function input(string $key, $default = null)
{
return self::instance()->input($key, $default);
}
/**
* Get request method
*/
public static function method(): string
{
return self::instance()->method();
}
/**
* Get request URI
*/
public static function uri(): string
{
return self::instance()->uri();
}
/**
* Check if request is AJAX
*/
public static function isAjax(): bool
{
return self::instance()->isAjax();
}
/**
* Check if request expects JSON
*/
public static function expectsJson(): bool
{
return self::instance()->expectsJson();
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Core\Facades;
/**
* NovaCore Response Facade
* Static access to response services
*/
class Response
{
/**
* Get response instance
*/
public static function instance(): \App\Core\Response
{
return app('response');
}
/**
* Set JSON response
*/
public static function json(array $data, int $status = 200): \App\Core\Response
{
return self::instance()->json($data, $status);
}
/**
* Set HTML response
*/
public static function html(string $content, int $status = 200): \App\Core\Response
{
return self::instance()->html($content, $status);
}
/**
* Redirect response
*/
public static function redirect(string $url, int $status = 302): void
{
self::instance()->redirect($url, $status);
}
/**
* Set status code
*/
public static function status(int $code): \App\Core\Response
{
return self::instance()->status($code);
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Core\Facades;
/**
* NovaCore Security Facade
* Static access to security services
*/
class Security
{
/**
* Get security instance
*/
public static function instance(): \App\Core\Security
{
return app('security');
}
/**
* Generate CSRF token
*/
public static function generateCsrfToken(): string
{
return self::instance()->generateCsrfToken();
}
/**
* Verify CSRF token
*/
public static function verifyCsrfToken(string $token): bool
{
return self::instance()->verifyCsrfToken($token);
}
/**
* Sanitize string input
*/
public static function sanitizeString(string $input): string
{
return self::instance()->sanitizeString($input);
}
/**
* Encrypt data
*/
public static function encrypt(string $data): string
{
return self::instance()->encrypt($data);
}
/**
* Decrypt data
*/
public static function decrypt(string $encryptedData): string
{
return self::instance()->decrypt($encryptedData);
}
/**
* Hash password
*/
public static function hashPassword(string $password): string
{
return self::instance()->hashPassword($password);
}
/**
* Verify password
*/
public static function verifyPassword(string $password, string $hash): bool
{
return self::instance()->verifyPassword($password, $hash);
}
}

42
app/Core/Facades/View.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Core\Facades;
/**
* NovaCore View Facade
* Static access to view services
*/
class View
{
/**
* Get view instance
*/
public static function instance(): \App\Core\View
{
return app('view');
}
/**
* Render a view
*/
public static function render(string $view, array $data = []): string
{
return self::instance()->render($view, $data);
}
/**
* Check if view exists
*/
public static function exists(string $view): bool
{
return self::instance()->exists($view);
}
/**
* Share data with all views
*/
public static function share(string $key, $value): void
{
self::instance()->share($key, $value);
}
}

62
app/Core/Middleware.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
namespace App\Core;
/**
* NovaCore Middleware Pipeline
* Stackable middleware system
*/
class Middleware
{
private array $middlewares = [];
/**
* Add middleware to stack
*/
public function add($middleware): void
{
$this->middlewares[] = $middleware;
}
/**
* Run middleware pipeline
*/
public function run(string $method, string $uri): void
{
$index = 0;
$this->executeMiddleware($index, $method, $uri);
}
/**
* Execute middleware recursively
*/
private function executeMiddleware(int &$index, string $method, string $uri): void
{
if ($index >= count($this->middlewares)) {
return;
}
$middleware = $this->middlewares[$index++];
if (is_string($middleware)) {
$middleware = new $middleware();
}
if (is_object($middleware) && method_exists($middleware, 'handle')) {
$middleware->handle($method, $uri, function () use (&$index, $method, $uri) {
$this->executeMiddleware($index, $method, $uri);
});
} else {
// Continue to next middleware
$this->executeMiddleware($index, $method, $uri);
}
}
/**
* Get all registered middlewares
*/
public function getMiddlewares(): array
{
return $this->middlewares;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Core\Middleware;
/**
* CSRF Middleware
* Cross-Site Request Forgery protection
*/
class CsrfMiddleware
{
public function handle(string $method, string $uri, callable $next): void
{
// Skip CSRF check for GET requests
if ($method === 'GET') {
$next();
return;
}
// Skip CSRF check for API routes (if Accept header is application/json)
if (isset($_SERVER['HTTP_ACCEPT']) && str_contains($_SERVER['HTTP_ACCEPT'], 'application/json')) {
$next();
return;
}
// Check CSRF token
$token = $_POST['_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? null;
if (!$token || !$this->verifyToken($token)) {
http_response_code(419);
echo "<h1>419 - Page Expired</h1>";
echo "<p>CSRF token mismatch. Please refresh the page and try again.</p>";
return;
}
// Continue to next middleware
$next();
}
/**
* Verify CSRF token
*/
private function verifyToken(string $token): bool
{
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Core\Middleware;
/**
* Security Middleware
* Basic security checks
*/
class SecurityMiddleware
{
public function handle(string $method, string $uri, callable $next): void
{
// Check for suspicious patterns
if ($this->isSuspiciousRequest($uri)) {
http_response_code(403);
echo "<h1>403 - Forbidden</h1>";
echo "<p>Access denied due to security policy.</p>";
return;
}
// Check request size
if ($this->isRequestTooLarge()) {
http_response_code(413);
echo "<h1>413 - Request Too Large</h1>";
echo "<p>Request size exceeds allowed limit.</p>";
return;
}
// Continue to next middleware
$next();
}
/**
* Check for suspicious request patterns
*/
private function isSuspiciousRequest(string $uri): bool
{
$suspiciousPatterns = [
'/\.\./', // Directory traversal
'/\.env/', // Environment file access
'/\.git/', // Git directory access
'/\.htaccess/', // Apache config access
'/\.htpasswd/', // Apache password file
'/admin\.php/', // Admin file access
'/config\.php/', // Config file access
'/wp-admin/', // WordPress admin
'/wp-login/', // WordPress login
'/phpmyadmin/', // phpMyAdmin
'/\.sql/', // SQL file access
'/\.bak/', // Backup file access
];
foreach ($suspiciousPatterns as $pattern) {
if (preg_match($pattern, $uri)) {
return true;
}
}
return false;
}
/**
* Check if request is too large
*/
private function isRequestTooLarge(): bool
{
$maxSize = 10 * 1024 * 1024; // 10MB
$contentLength = (int)($_SERVER['CONTENT_LENGTH'] ?? 0);
return $contentLength > $maxSize;
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Core\Providers;
/**
* NovaCore Application Service Provider
* Register application services
*/
class AppServiceProvider
{
/**
* Register services
*/
public function register(): void
{
// Register core services
$this->registerCoreServices();
}
/**
* Boot services
*/
public function boot(): void
{
// Boot services
}
/**
* Register core services
*/
private function registerCoreServices(): void
{
// This will be called by the Bootstrap class
// to register all core services
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Core\Providers;
/**
* NovaCore Security Service Provider
* Register security services
*/
class SecurityServiceProvider
{
/**
* Register services
*/
public function register(): void
{
// Register security services
}
/**
* Boot services
*/
public function boot(): void
{
// Boot security services
}
}

197
app/Core/Request.php Normal file
View File

@@ -0,0 +1,197 @@
<?php
namespace App\Core;
/**
* NovaCore Request Handler
* HTTP request wrapper
*/
class Request
{
private array $data;
public function __construct()
{
$this->data = array_merge($_GET, $_POST, $_REQUEST);
}
/**
* Get all input data
*/
public function all(): array
{
return $this->data;
}
/**
* Get input value by key
*/
public function input(string $key, $default = null)
{
return $this->data[$key] ?? $default;
}
/**
* Get only specified keys
*/
public function only(array $keys): array
{
return array_intersect_key($this->data, array_flip($keys));
}
/**
* Get all except specified keys
*/
public function except(array $keys): array
{
return array_diff_key($this->data, array_flip($keys));
}
/**
* Check if key exists
*/
public function has(string $key): bool
{
return isset($this->data[$key]);
}
/**
* Get request method
*/
public function method(): string
{
return $_SERVER['REQUEST_METHOD'] ?? 'GET';
}
/**
* Check if method is POST
*/
public function isPost(): bool
{
return $this->method() === 'POST';
}
/**
* Check if method is GET
*/
public function isGet(): bool
{
return $this->method() === 'GET';
}
/**
* Get request URI
*/
public function uri(): string
{
return $_SERVER['REQUEST_URI'] ?? '/';
}
/**
* Get request path
*/
public function path(): string
{
return parse_url($this->uri(), PHP_URL_PATH);
}
/**
* Get query string
*/
public function query(): string
{
return $_SERVER['QUERY_STRING'] ?? '';
}
/**
* Get headers
*/
public function headers(): array
{
if (function_exists('getallheaders')) {
$h = getallheaders();
return is_array($h) ? $h : [];
}
// Fallback build from $_SERVER
$headers = [];
foreach ($_SERVER as $key => $value) {
if (strpos($key, 'HTTP_') === 0) {
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))));
$headers[$name] = $value;
} elseif (in_array($key, ['CONTENT_TYPE', 'CONTENT_LENGTH'])) {
$name = str_replace('_', '-', ucwords(strtolower($key), '_'));
$headers[$name] = $value;
}
}
return $headers;
}
/**
* Get specific header
*/
public function header(string $name): ?string
{
$headers = $this->headers();
return $headers[$name] ?? null;
}
/**
* Check if request is AJAX
*/
public function isAjax(): bool
{
return $this->header('X-Requested-With') === 'XMLHttpRequest';
}
/**
* Check if request expects JSON
*/
public function expectsJson(): bool
{
$accept = $this->header('Accept') ?? '';
return stripos($accept, 'application/json') !== false;
}
/**
* Get file upload
*/
public function file(string $key): ?array
{
return $_FILES[$key] ?? null;
}
/**
* Get all files
*/
public function files(): array
{
return $_FILES;
}
/**
* Get IP address
*/
public function ip(): string
{
return $_SERVER['HTTP_X_FORWARDED_FOR'] ??
$_SERVER['HTTP_X_REAL_IP'] ??
$_SERVER['REMOTE_ADDR'] ??
'unknown';
}
/**
* Get user agent
*/
public function userAgent(): string
{
return $_SERVER['HTTP_USER_AGENT'] ?? '';
}
/**
* Get referer
*/
public function referer(): ?string
{
return $_SERVER['HTTP_REFERER'] ?? null;
}
}

133
app/Core/Response.php Normal file
View File

@@ -0,0 +1,133 @@
<?php
namespace App\Core;
/**
* NovaCore Response Handler
* HTTP response wrapper
*/
class Response
{
private int $statusCode = 200;
private array $headers = [];
private $content = '';
/**
* Set status code
*/
public function status(int $code): self
{
$this->statusCode = $code;
return $this;
}
/**
* Set header
*/
public function header(string $name, string $value): self
{
$this->headers[$name] = $value;
return $this;
}
/**
* Set content type
*/
public function contentType(string $type): self
{
return $this->header('Content-Type', $type);
}
/**
* Set JSON response
*/
public function json(array $data, int $status = 200): self
{
$this->statusCode = $status;
$this->contentType('application/json');
$this->content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
return $this;
}
/**
* Set HTML response
*/
public function html(string $content, int $status = 200): self
{
$this->statusCode = $status;
$this->contentType('text/html; charset=utf-8');
$this->content = $content;
return $this;
}
/**
* Set plain text response
*/
public function text(string $content, int $status = 200): self
{
$this->statusCode = $status;
$this->contentType('text/plain; charset=utf-8');
$this->content = $content;
return $this;
}
/**
* Redirect response
*/
public function redirect(string $url, int $status = 302): void
{
$this->statusCode = $status;
$this->header('Location', $url);
$this->send();
}
/**
* Set content
*/
public function content($content): self
{
$this->content = $content;
return $this;
}
/**
* Send response
*/
public function send(): void
{
// Set status code
http_response_code($this->statusCode);
// Set headers
foreach ($this->headers as $name => $value) {
header("{$name}: {$value}");
}
// Output content
echo $this->content;
}
/**
* Get status code
*/
public function getStatusCode(): int
{
return $this->statusCode;
}
/**
* Get headers
*/
public function getHeaders(): array
{
return $this->headers;
}
/**
* Get content
*/
public function getContent()
{
return $this->content;
}
}

145
app/Core/Router.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
namespace App\Core;
/**
* NovaCore Router
* Simple FastRoute-like router
*/
class Router
{
private array $routes = [];
private array $patterns = [
'{id}' => '([0-9]+)',
'{slug}' => '([a-zA-Z0-9\-]+)',
'{any}' => '(.+)'
];
/**
* Register a GET route
*/
public function get(string $path, string $handler): void
{
$this->addRoute('GET', $path, $handler);
}
/**
* Register a POST route
*/
public function post(string $path, string $handler): void
{
$this->addRoute('POST', $path, $handler);
}
/**
* Register a PUT route
*/
public function put(string $path, string $handler): void
{
$this->addRoute('PUT', $path, $handler);
}
/**
* Register a DELETE route
*/
public function delete(string $path, string $handler): void
{
$this->addRoute('DELETE', $path, $handler);
}
/**
* Add route to collection
*/
private function addRoute(string $method, string $path, string $handler): void
{
$this->routes[$method][] = [
'path' => $path,
'handler' => $handler,
'pattern' => $this->compilePattern($path),
'params' => $this->extractParams($path)
];
}
/**
* Compile route pattern for regex matching
*/
private function compilePattern(string $path): string
{
// First replace placeholders with regex patterns
$pattern = $path;
foreach ($this->patterns as $placeholder => $regex) {
$pattern = str_replace($placeholder, $regex, $pattern);
}
// Escape only the non-regex parts
$pattern = preg_quote($pattern, '/');
// Restore the regex patterns that were escaped
foreach ($this->patterns as $placeholder => $regex) {
$escapedRegex = preg_quote($regex, '/');
$pattern = str_replace($escapedRegex, $regex, $pattern);
}
return '/^' . $pattern . '$/';
}
/**
* Extract parameter names from route
*/
private function extractParams(string $path): array
{
preg_match_all('/\{([^}]+)\}/', $path, $matches);
return $matches[1] ?? [];
}
/**
* Match request against routes
*/
public function match(string $method, string $uri): ?array
{
if (!isset($this->routes[$method])) {
return null;
}
foreach ($this->routes[$method] as $route) {
if (preg_match($route['pattern'], $uri, $matches)) {
// Remove full match, keep only captured groups
array_shift($matches);
// Map parameters
$params = [];
foreach ($route['params'] as $index => $paramName) {
$params[$paramName] = $matches[$index] ?? null;
}
return [
'handler' => $route['handler'],
'params' => $params,
'module' => $this->extractModule($route['handler'])
];
}
}
return null;
}
/**
* Extract module name from handler
*/
private function extractModule(string $handler): string
{
// Handler formats supported:
// - "Home\\Controller@index" → module "Home"
// - "Controller@index" when module implied by route context
$parts = explode('\\', $handler);
return $parts[0] ?: 'Default';
}
/**
* Get all registered routes
*/
public function getRoutes(): array
{
return $this->routes;
}
}

187
app/Core/Security.php Normal file
View File

@@ -0,0 +1,187 @@
<?php
namespace App\Core;
/**
* NovaCore Security Helper
* XSS, CSRF, and other security features
*/
class Security
{
private string $appKey;
private string $csrfTokenName;
public function __construct()
{
$this->appKey = getenv('APP_KEY') ?: 'default-key-change-in-production';
$this->csrfTokenName = getenv('CSRF_TOKEN_NAME') ?: '_token';
}
/**
* Initialize security features
*/
public function initialize(): void
{
// Set security headers
$this->setSecurityHeaders();
// Sanitize input
$this->sanitizeInput();
// Generate CSRF token if needed
$this->ensureCsrfToken();
}
/**
* Set security headers
*/
private function setSecurityHeaders(): void
{
// Prevent XSS
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
// Content Security Policy - Allow external CDNs for development
if (getenv('APP_ENV') === 'development' || getenv('APP_DEBUG') === 'true') {
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.tailwindcss.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.tailwindcss.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self'");
} else {
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'");
}
// Strict Transport Security (HTTPS only)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
}
/**
* Sanitize all input data
*/
private function sanitizeInput(): void
{
$_GET = $this->sanitizeArray($_GET);
$_POST = $this->sanitizeArray($_POST);
$_REQUEST = $this->sanitizeArray($_REQUEST);
}
/**
* Sanitize array recursively
*/
private function sanitizeArray(array $data): array
{
foreach ($data as $key => $value) {
if (is_array($value)) {
$data[$key] = $this->sanitizeArray($value);
} else {
$data[$key] = $this->sanitizeString($value);
}
}
return $data;
}
/**
* Sanitize string input
*/
public function sanitizeString(string $input): string
{
// Remove null bytes
$input = str_replace(chr(0), '', $input);
// HTML encode special characters
$input = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
return $input;
}
/**
* Generate CSRF token
*/
public function generateCsrfToken(): string
{
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
/**
* Verify CSRF token
*/
public function verifyCsrfToken(string $token): bool
{
if (!isset($_SESSION['csrf_token'])) {
return false;
}
return hash_equals($_SESSION['csrf_token'], $token);
}
/**
* Ensure CSRF token exists
*/
private function ensureCsrfToken(): void
{
if (!isset($_SESSION['csrf_token'])) {
$this->generateCsrfToken();
}
}
/**
* Get CSRF token name
*/
public function getCsrfTokenName(): string
{
return $this->csrfTokenName;
}
/**
* Encrypt data using AES-256-GCM
*/
public function encrypt(string $data): string
{
$key = hash('sha256', $this->appKey, true);
$iv = random_bytes(16);
$ciphertext = openssl_encrypt($data, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
return base64_encode($iv . $tag . $ciphertext);
}
/**
* Decrypt data using AES-256-GCM
*/
public function decrypt(string $encryptedData): string
{
$data = base64_decode($encryptedData);
$key = hash('sha256', $this->appKey, true);
$iv = substr($data, 0, 16);
$tag = substr($data, 16, 16);
$ciphertext = substr($data, 32);
return openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
}
/**
* Hash password securely
*/
public function hashPassword(string $password): string
{
return password_hash($password, PASSWORD_ARGON2ID);
}
/**
* Verify password
*/
public function verifyPassword(string $password, string $hash): bool
{
return password_verify($password, $hash);
}
/**
* Generate secure random string
*/
public function generateRandomString(int $length = 32): string
{
return bin2hex(random_bytes($length));
}
}

160
app/Core/View.php Normal file
View File

@@ -0,0 +1,160 @@
<?php
namespace App\Core;
/**
* NovaCore View Renderer
* Simple PHP template engine
*/
class View
{
private string $viewPath;
private array $sharedData = [];
public function __construct()
{
$this->viewPath = __DIR__ . '/../Modules';
}
/**
* Render a view
*/
public function render(string $view, array $data = []): string
{
$data = array_merge($this->sharedData, $data);
// Get view file path
$viewFile = $this->getViewFile($view);
if (!file_exists($viewFile)) {
throw new \Exception("View '{$view}' not found");
}
// Read view file content
$content = file_get_contents($viewFile);
// Process template syntax first
$content = $this->processTemplate($content);
// Debug: Log processed content
if (getenv('APP_DEBUG') === 'true') {
error_log("Processed template content: " . substr($content, 0, 500));
}
// Extract data to variables
extract($data);
// Start output buffering
ob_start();
// Evaluate processed content
try {
eval('?>' . $content);
} catch (ParseError $e) {
// Log the problematic content for debugging
error_log("Template parse error: " . $e->getMessage());
error_log("Problematic content around line " . $e->getLine() . ": " . substr($content, max(0, $e->getLine() - 10) * 50, 1000));
throw $e;
}
// Get content and clean buffer
return ob_get_clean();
}
/**
* Get view file path
*/
private function getViewFile(string $view): string
{
// Convert dot notation to path
$view = str_replace('.', '/', $view);
// Add .php extension if not present
if (!str_ends_with($view, '.php')) {
$view .= '.php';
}
return $this->viewPath . '/' . $view;
}
/**
* Process template syntax
*/
private function processTemplate(string $content): string
{
// Process {{ }} syntax (auto-escape)
$content = preg_replace_callback('/\{\{\s*(.+?)\s*\}\}/', function ($matches) {
$expression = trim($matches[1]);
return '<?php echo htmlspecialchars(' . $expression . ', ENT_QUOTES, \'UTF-8\'); ?>';
}, $content);
// Process {!! !!} syntax (raw output)
$content = preg_replace_callback('/\{!!\s*(.+?)\s*!!\}/', function ($matches) {
$expression = trim($matches[1]);
return '<?php echo ' . $expression . '; ?>';
}, $content);
// Process @if statements
$content = preg_replace('/@if\s*\(\s*([^)]+)\s*\)/', '<?php if ($1): ?>', $content);
$content = preg_replace('/@elseif\s*\(\s*([^)]+)\s*\)/', '<?php elseif ($1): ?>', $content);
$content = preg_replace('/@else/', '<?php else: ?>', $content);
$content = preg_replace('/@endif/', '<?php endif; ?>', $content);
// Debug: Log processed @if statements
if (getenv('APP_DEBUG') === 'true') {
error_log("After @if processing: " . substr($content, 0, 1000));
}
// Process @foreach loops
$content = preg_replace('/@foreach\s*\((.+?)\)/', '<?php foreach ($1): ?>', $content);
$content = preg_replace('/@endforeach/', '<?php endforeach; ?>', $content);
// Process @for loops
$content = preg_replace('/@for\s*\((.+?)\)/', '<?php for ($1): ?>', $content);
$content = preg_replace('/@endfor/', '<?php endfor; ?>', $content);
// Process @while loops
$content = preg_replace('/@while\s*\((.+?)\)/', '<?php while ($1): ?>', $content);
$content = preg_replace('/@endwhile/', '<?php endwhile; ?>', $content);
// Process @csrf directive
$content = preg_replace('/@csrf/', '<?php echo csrf_token(); ?>', $content);
// Process @method directive
$content = preg_replace('/@method\s*\((.+?)\)/', '<?php echo method_field($1); ?>', $content);
return $content;
}
/**
* Share data with all views
*/
public function share(string $key, $value): void
{
$this->sharedData[$key] = $value;
}
/**
* Check if view exists
*/
public function exists(string $view): bool
{
$viewFile = $this->getViewFile($view);
return file_exists($viewFile);
}
/**
* Get view path
*/
public function getViewPath(): string
{
return $this->viewPath;
}
/**
* Set view path
*/
public function setViewPath(string $path): void
{
$this->viewPath = $path;
}
}

200
app/Core/helpers.php Normal file
View File

@@ -0,0 +1,200 @@
<?php
/**
* NovaCore Framework Core Helper Functions
* Additional utility functions
*/
if (!function_exists('storage_path')) {
/**
* Get storage path
*/
function storage_path(string $path = ''): string
{
$storagePath = __DIR__ . '/../../storage';
return $path ? $storagePath . '/' . ltrim($path, '/') : $storagePath;
}
}
if (!function_exists('database_path')) {
/**
* Get database path
*/
function database_path(string $path = ''): string
{
$dbPath = __DIR__ . '/../../database';
return $path ? $dbPath . '/' . ltrim($path, '/') : $dbPath;
}
}
if (!function_exists('base_path')) {
/**
* Get base path
*/
function base_path(string $path = ''): string
{
$basePath = __DIR__ . '/../..';
return $path ? $basePath . '/' . ltrim($path, '/') : $basePath;
}
}
if (!function_exists('app_path')) {
/**
* Get app path
*/
function app_path(string $path = ''): string
{
$appPath = __DIR__ . '/..';
return $path ? $appPath . '/' . ltrim($path, '/') : $appPath;
}
}
if (!function_exists('public_path')) {
/**
* Get public path
*/
function public_path(string $path = ''): string
{
$publicPath = __DIR__ . '/../../public';
return $path ? $publicPath . '/' . ltrim($path, '/') : $publicPath;
}
}
if (!function_exists('config_path')) {
/**
* Get config path
*/
function config_path(string $path = ''): string
{
$configPath = __DIR__ . '/../Config';
return $path ? $configPath . '/' . ltrim($path, '/') : $configPath;
}
}
if (!function_exists('is_production')) {
/**
* Check if running in production
*/
function is_production(): bool
{
return env('APP_ENV', 'production') === 'production';
}
}
if (!function_exists('is_development')) {
/**
* Check if running in development
*/
function is_development(): bool
{
return env('APP_ENV', 'production') === 'development';
}
}
if (!function_exists('is_testing')) {
/**
* Check if running in testing
*/
function is_testing(): bool
{
return env('APP_ENV', 'production') === 'testing';
}
}
if (!function_exists('abort')) {
/**
* Abort with error code
*/
function abort(int $code, string $message = ''): void
{
http_response_code($code);
if ($message) {
echo "<h1>{$code} - Error</h1>";
echo "<p>{$message}</p>";
} else {
switch ($code) {
case 404:
echo "<h1>404 - Not Found</h1>";
echo "<p>The requested page could not be found.</p>";
break;
case 500:
echo "<h1>500 - Internal Server Error</h1>";
echo "<p>Something went wrong on our end.</p>";
break;
default:
echo "<h1>{$code} - Error</h1>";
break;
}
}
exit;
}
}
if (!function_exists('logger')) {
/**
* Log message
*/
function logger(string $message, string $level = 'info'): void
{
$logFile = storage_path('logs/error.log');
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] [{$level}] {$message}" . PHP_EOL;
file_put_contents($logFile, $logMessage, FILE_APPEND | LOCK_EX);
}
}
if (!function_exists('cache')) {
/**
* Simple cache helper
*/
function cache(string $key, $value = null, int $ttl = 3600)
{
$cacheFile = storage_path("cache/{$key}.cache");
if ($value === null) {
// Get from cache
if (file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $ttl) {
return unserialize(file_get_contents($cacheFile));
}
return null;
} else {
// Set cache
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
file_put_contents($cacheFile, serialize($value));
return $value;
}
}
}
if (!function_exists('cache_forget')) {
/**
* Remove from cache
*/
function cache_forget(string $key): bool
{
$cacheFile = storage_path("cache/{$key}.cache");
return file_exists($cacheFile) ? unlink($cacheFile) : false;
}
}
if (!function_exists('cache_flush')) {
/**
* Clear all cache
*/
function cache_flush(): void
{
$cacheDir = storage_path('cache');
if (is_dir($cacheDir)) {
$files = glob($cacheDir . '/*.cache');
foreach ($files as $file) {
unlink($file);
}
}
}
}