diff --git a/Engine/App.php b/Engine/App.php index 7d331b2..e9c5720 100755 --- a/Engine/App.php +++ b/Engine/App.php @@ -4,6 +4,7 @@ use Engine\Error\ShiftError; use ReflectionClass; +use ReflectionException; /** * Class App @@ -11,47 +12,77 @@ */ final class App { - /** - * @var - */ - public static $controller; - public static $defaultController = 'index'; - public static $action; - public static $defaultAction = 'index'; - + private Request $request; + private ServiceContainer $container; + private string $defaultController = 'index'; + private string $defaultAction = 'index'; + private bool $isRunning = false; - /** - * App constructor. - */ - public function __construct() + public function __construct(Request $request) { + $this->request = $request; + $this->container = new ServiceContainer(); + $this->registerDefaultServices(); } /** - * @throws \ReflectionException + * @throws ShiftError */ - public static function start(): void + public function start(): void { - static $run; - if ($run === TRUE) return; - - Request::setup(); - $controller = 'Controllers\\' . ucfirst(Request::getController()) . 'Controller'; + if ($this->isRunning) { + return; + } try { - self::$controller = new $controller(); + $controller = $this->resolveController(); + $this->executeController($controller); + $this->isRunning = true; + } catch (ReflectionException $exception) { + throw new ShiftError( + 'Controller method not found: ' . $exception->getMessage(), + $exception->getCode(), + $exception->getPrevious() + ); } catch (\Throwable $exception) { - throw new ShiftError($exception->getMessage(), $exception->getCode(), $exception->getPrevious()); + throw new ShiftError( + 'Application error: ' . $exception->getMessage(), + $exception->getCode(), + $exception->getPrevious() + ); } + } - $class = new ReflectionClass(self::$controller); + /** + * @throws ShiftError + */ + private function resolveController(): object + { + $controllerName = ucfirst($this->request->getController()) . 'Controller'; + $controllerClass = 'Controllers\\' . $controllerName; - $method = $class->getMethod(Request::getAction()); + if (!class_exists($controllerClass)) { + throw new ShiftError("Controller class '{$controllerClass}' not found"); + } - $method->invokeArgs(self::$controller, Request::getArguments()); - $run = TRUE; + return new $controllerClass(); } + /** + * @throws ReflectionException + */ + private function executeController(object $controller): void + { + $reflectionClass = new ReflectionClass($controller); + $methodName = $this->request->getAction(); + + if (!$reflectionClass->hasMethod($methodName)) { + throw new ReflectionException("Method '{$methodName}' not found in controller"); + } + + $method = $reflectionClass->getMethod($methodName); + $method->invokeArgs($controller, $this->request->getArguments()); + } /** * @param string $class_name @@ -77,11 +108,46 @@ public static function autoload(string $class_name): void require_once($location . $class . '.php'); } } - } public static function setHelpers(): void { require_once 'Utils/helpers.php'; } + + public function getRequest(): Request + { + return $this->request; + } + + public function setDefaultController(string $controller): void + { + $this->defaultController = $controller; + } + + public function setDefaultAction(string $action): void + { + $this->defaultAction = $action; + } + + private function registerDefaultServices(): void + { + $this->container->singleton('request', $this->request); + $this->container->singleton('view', function() { + return new View(); + }); + $this->container->singleton('storage', function() { + return new \Engine\Utils\Storage(); + }); + } + + public function getContainer(): ServiceContainer + { + return $this->container; + } + + public function resolve(string $service) + { + return $this->container->resolve($service); + } } diff --git a/Engine/Console/Cli.php b/Engine/Console/Cli.php new file mode 100644 index 0000000..5a35cfc --- /dev/null +++ b/Engine/Console/Cli.php @@ -0,0 +1,263 @@ +output(self::$COLOR_BLUE . $message . self::$COLOR_DEFAULT, $newLine); + } + + /** + * Wyświetla informację debugowania w standardowym kolorze + * + * @param string $message Treść wiadomości + * @param bool $newLine Czy dodać nową linię na końcu + * @return void + */ + public function debug(string $message, bool $newLine = true): void + { + $this->output($message, $newLine); + } + + /** + * Wyświetla informację o błędzie w kolorze czerwonym + * + * @param string $message Treść wiadomości + * @param bool $newLine Czy dodać nową linię na końcu + * @return void + */ + public function error(string $message, bool $newLine = true): void + { + $this->output(self::$COLOR_RED . $message . self::$COLOR_DEFAULT, $newLine); + } + + /** + * Wyświetla informację o sukcesie w kolorze zielonym + * + * @param string $message Treść wiadomości + * @param bool $newLine Czy dodać nową linię na końcu + * @return void + */ + public function success(string $message, bool $newLine = true): void + { + $this->output(self::$COLOR_GREEN . $message . self::$COLOR_DEFAULT, $newLine); + } + + /** + * Wyświetla ostrzeżenie w kolorze żółtym + * + * @param string $message Treść wiadomości + * @param bool $newLine Czy dodać nową linię na końcu + * @return void + */ + public function warning(string $message, bool $newLine = true): void + { + $this->output(self::$COLOR_YELLOW . $message . self::$COLOR_DEFAULT, $newLine); + } + + /** + * Wyświetla linię separatora + * + * @param int $length Długość linii + * @param string $char Znak używany do narysowania linii + * @return void + */ + public function line(int $length = 50, string $char = '-'): void + { + $this->output(str_repeat($char, $length)); + } + + /** + * Wyświetla tekst z tabulacją + * + * @param string $message Treść wiadomości + * @param int $level Poziom wcięcia + * @param bool $newLine Czy dodać nową linię na końcu + * @return void + */ + public function indent(string $message, int $level = 1, bool $newLine = true): void + { + $indent = str_repeat(" ", $level); // dwa spacje na poziom + $this->output($indent . $message, $newLine); + } + + /** + * Wyświetla tabelę danych + * + * @param array $headers Nagłówki tabeli + * @param array $rows Wiersze z danymi + * @return void + */ + public function table(array $headers, array $rows): void + { + // Obliczanie szerokości kolumn + $columnWidths = []; + foreach ($headers as $i => $header) { + $columnWidths[$i] = strlen($header); + foreach ($rows as $row) { + if (isset($row[$i]) && strlen($row[$i]) > $columnWidths[$i]) { + $columnWidths[$i] = strlen($row[$i]); + } + } + } + + // Rysowanie górnej granicy + $this->drawTableBorder($columnWidths); + + // Rysowanie nagłówków + $this->drawTableRow($headers, $columnWidths); + + // Rysowanie linii rozdzielającej + $this->drawTableBorder($columnWidths); + + // Rysowanie wierszy z danymi + foreach ($rows as $row) { + $this->drawTableRow($row, $columnWidths); + } + + // Rysowanie dolnej granicy + $this->drawTableBorder($columnWidths); + } + + /** + * Wyświetla postęp operacji + * + * @param int $current Aktualna wartość + * @param int $total Całkowita wartość + * @param int $barWidth Szerokość paska postępu + * @return void + */ + public function progressBar(int $current, int $total, int $barWidth = 50): void + { + $percent = $current / $total; + $bar = floor($percent * $barWidth); + $status = str_pad("[", 1); + $status .= str_repeat("=", $bar); + if ($bar < $barWidth) { + $status .= ">"; + $status .= str_repeat(" ", $barWidth - $bar - 1); + } else { + $status .= "="; + } + $status .= "]"; + $status .= " " . number_format($percent * 100, 0) . "%"; + $status .= " $current/$total"; + + echo "\r"; // Powrót karetki + $this->output($status, false); + + if ($current === $total) { + echo "\n"; // Nowa linia na końcu postępu + } + + // Sprawdzamy czy bufor jest aktywny przed próbą jego opróżnienia + if (ob_get_level() > 0) { + ob_flush(); + } + flush(); + } + + /** + * Pobiera wejście od użytkownika + * + * @param string $prompt Zapytanie wyświetlane użytkownikowi + * @param string|null $default Domyślna wartość + * @return string Wprowadzona wartość + */ + public function input(string $prompt, ?string $default = null): string + { + $promptText = $prompt; + if ($default !== null) { + $promptText .= " [" . $default . "]"; + } + $promptText .= ": "; + + $this->output($promptText, false); + $input = trim(fgets(STDIN)); + + if ($input === '' && $default !== null) { + return $default; + } + + return $input; + } + + /** + * Pobiera potwierdzenie od użytkownika + * + * @param string $prompt Zapytanie wyświetlane użytkownikowi + * @param bool $default Domyślna wartość + * @return bool Wynik potwierdzenia + */ + public function confirm(string $prompt, bool $default = true): bool + { + $defaultText = $default ? 'T' : 'N'; + $options = $default ? '[T/n]' : '[t/N]'; + + $input = $this->input("$prompt $options", $defaultText); + return in_array(strtolower($input), ['t', 'tak', 'y', 'yes']); + } + + /** + * Wyświetla wiadomość + * + * @param string $message Treść wiadomości + * @param bool $newLine Czy dodać nową linię + * @return void + */ + private function output(string $message, bool $newLine = true): void + { + echo $message . ($newLine ? PHP_EOL : ''); + } + + /** + * Rysuje wiersz tabeli + * + * @param array $data Dane wiersza + * @param array $columnWidths Szerokości kolumn + * @return void + */ + private function drawTableRow(array $data, array $columnWidths): void + { + echo "|"; + foreach ($columnWidths as $i => $width) { + $value = $data[$i] ?? ''; + echo " " . str_pad($value, $width) . " |"; + } + echo PHP_EOL; + } + + /** + * Rysuje granicę tabeli + * + * @param array $columnWidths Szerokości kolumn + * @return void + */ + private function drawTableBorder(array $columnWidths): void + { + echo "+"; + foreach ($columnWidths as $width) { + echo "-" . str_repeat("-", $width) . "-+"; + } + echo PHP_EOL; + } +} diff --git a/Engine/Console/CommandInterface.php b/Engine/Console/CommandInterface.php index 1deff85..65d9db3 100644 --- a/Engine/Console/CommandInterface.php +++ b/Engine/Console/CommandInterface.php @@ -11,9 +11,9 @@ interface CommandInterface { - public function execute(...$args): void; + public function execute(mixed ...$args): void; - public function getHelp(); + public function getHelp(): string; - public function getDescription(); + public function getDescription(): string; } diff --git a/Engine/Console/Commands/Help.php b/Engine/Console/Commands/Help.php index 0265efb..8429b74 100644 --- a/Engine/Console/Commands/Help.php +++ b/Engine/Console/Commands/Help.php @@ -8,13 +8,12 @@ namespace Console\Commands; - use Engine\Console\CommandInterface; class Help implements CommandInterface { - - private $mappings = [ + /** @var array */ + private array $mappings = [ [ 'dir' => APP_PATH . '/console/', 'namespace' => 'AppConsole\\Commands\\' @@ -28,7 +27,7 @@ class Help implements CommandInterface /** * @param mixed ...$args */ - public function execute(...$args): void + public function execute(mixed ...$args): void { $commandName = $args[0] ?? ''; @@ -39,9 +38,11 @@ public function execute(...$args): void } } - private function displayHelpForCommand($command) + /** + * @param string $command + */ + private function displayHelpForCommand(string $command): void { - $found = false; foreach ($this->mappings as $mapping) { @@ -52,8 +53,25 @@ private function displayHelpForCommand($command) } } - private function displayFullHelp() + private function displayFullHelp(): void + { + } + + /** + * @return string + */ + public function getHelp(): string { + // TODO: Implement getHelp() method. + return ''; + } + /** + * @return string + */ + public function getDescription(): string + { + // TODO: Implement getDescription() method. + return ''; } } diff --git a/Engine/Console/Commands/Serve.php b/Engine/Console/Commands/Serve.php index 63002d5..f0682cb 100644 --- a/Engine/Console/Commands/Serve.php +++ b/Engine/Console/Commands/Serve.php @@ -15,13 +15,11 @@ class Serve implements CommandInterface { - public function execute(...$args): void + public function execute(mixed ...$args): void { $host = 'localhost:8000'; - if (count($args)) { - if (isset($args[0])) { - $host = $args[0]; - } + if (count($args) && isset($args[0])) { + $host = $args[0]; } $command = $this->getOpenCommand() . ' http://' . $host . ';php -S ' . $host; @@ -29,7 +27,7 @@ public function execute(...$args): void exec($command); } - private final function getOpenCommand(): string + private function getOpenCommand(): string { $command = 'open'; @@ -45,13 +43,15 @@ private final function getOpenCommand(): string return $command; } - public function getHelp() + public function getHelp(): string { // TODO: Implement getHelp() method. + return ''; } - public function getDescription() + public function getDescription(): string { // TODO: Implement getDescription() method. + return ''; } } diff --git a/Engine/Console/Console.php b/Engine/Console/Console.php index f45c060..fe4e82a 100644 --- a/Engine/Console/Console.php +++ b/Engine/Console/Console.php @@ -4,17 +4,39 @@ * User: dawidjez * Date: 14/12/2018 * Time: 12:09 + * Updated: 2025-06-22 */ namespace Engine\Console; - +/** + * Główna klasa konsolowa + */ class Console { + /** + * Instancja klasy Cli do operacji terminalowych + * + * @var Cli + */ + private Cli $cli; - public function __constructor() + /** + * Konstruktor klasy Console + */ + public function __construct() { + $this->cli = new Cli(); + } + /** + * Zwraca instancję klasy Cli + * + * @return Cli + */ + public function cli(): Cli + { + return $this->cli; } } diff --git a/Engine/Console/Shift.php b/Engine/Console/Shift.php index 2a99a13..84eb320 100644 --- a/Engine/Console/Shift.php +++ b/Engine/Console/Shift.php @@ -9,13 +9,13 @@ namespace Engine\Console; -use Grabower\CliTypo\CliTypo; use ReflectionClass; +use ReflectionException; class Shift { - protected $_description = 'xd'; - private $_args = []; + protected string $_description = 'xd'; + private array $_args = []; public function __construct(array $argv) { @@ -26,9 +26,9 @@ public function __construct(array $argv) * @param string $name * @return mixed|null */ - public function getArg(string $name) + public function getArg(string $name): mixed { - return isset($this->_args[$name]) ? $this->_args[$name] : null; + return $this->_args[$name] ?? null; } /** @@ -47,15 +47,19 @@ public function setArgs(array $args): void $this->_args = $args; } + /** + * @throws ReflectionException + * @return void + */ public function run(): void { - $cliTypo = new CliTypo(); + $cli = new Cli(); if (count($this->_args) < 2) { - $cliTypo->alert()->error('Shift CLI needs at least one parameter'); + $cli->error('Shift CLI needs at least one parameter'); exit(); } $commandName = ucfirst($this->_args[1]); - $cliTypo->text()->write($commandName); + $cli->info($commandName); $mappings = [ [ @@ -75,7 +79,7 @@ public function run(): void } } if (!$found) { - $cliTypo->alert()->error('Command ' . $commandName . ' not found'); + $cli->error('Command ' . $commandName . ' not found'); exit(); } diff --git a/Engine/Controller.php b/Engine/Controller.php index b2087a4..d00c6fc 100755 --- a/Engine/Controller.php +++ b/Engine/Controller.php @@ -6,8 +6,16 @@ * Class Controller * @package Engine */ -class Controller +abstract class Controller { + protected Request $request; + protected View $view; + + public function __construct() + { + $this->request = new Request(); + $this->view = new View(); + } /** * @param string $view @@ -17,8 +25,44 @@ class Controller * @param array $scripts * @return void */ - public function render(string $view, array $data = [], string $title = '', array $styles = [], array $scripts = []): void + protected function render(string $view, array $data = [], string $title = '', array $styles = [], array $scripts = []): void + { + $this->view->make($view, $data, $title, $styles, $scripts); + } + + /** + * @param array $data + * @return void + */ + protected function json(array $data): void + { + header('Content-Type: application/json'); + echo json_encode($data); + } + + /** + * @param string $url + * @return void + */ + protected function redirect(string $url): void + { + header("Location: {$url}"); + exit; + } + + /** + * @return Request + */ + protected function getRequest(): Request + { + return $this->request; + } + + /** + * @return View + */ + protected function getView(): View { - (new View)->make($view, $data, $title, $styles, $scripts); + return $this->view; } } diff --git a/Engine/Error/ErrorHandler.php b/Engine/Error/ErrorHandler.php new file mode 100644 index 0000000..f491448 --- /dev/null +++ b/Engine/Error/ErrorHandler.php @@ -0,0 +1,111 @@ +setFile($file); + $exception->setLine($line); + + self::handleException($exception); + return true; + } + + return false; + } + + public static function handleException(Throwable $exception): void + { + if (self::$customHandler) { + call_user_func(self::$customHandler, $exception); + return; + } + + if (php_sapi_name() === 'cli') { + self::renderCliError($exception); + } else { + self::renderWebError($exception); + } + } + + public static function handleShutdown(): void + { + $error = error_get_last(); + + if ($error !== null && in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR])) { + $exception = new ShiftError($error['message'], $error['type']); + $exception->setFile($error['file']); + $exception->setLine($error['line']); + + self::handleException($exception); + } + } + + private static function renderCliError(Throwable $exception): void + { + echo "\n"; + echo "Fatal Error: " . $exception->getMessage() . "\n"; + echo "File: " . $exception->getFile() . "\n"; + echo "Line: " . $exception->getLine() . "\n"; + echo "\nStack Trace:\n"; + echo $exception->getTraceAsString() . "\n"; + echo "\n"; + } + + private static function renderWebError(Throwable $exception): void + { + if ($exception instanceof ShiftError) { + // Use the existing ShiftError rendering + return; + } + + // Simple error rendering for other exceptions + http_response_code(500); + + if (ini_get('display_errors')) { + echo '

Application Error

'; + echo '

Message: ' . htmlspecialchars($exception->getMessage()) . '

'; + echo '

File: ' . htmlspecialchars($exception->getFile()) . '

'; + echo '

Line: ' . $exception->getLine() . '

'; + + if (ini_get('display_errors') === '1') { + echo '

Stack Trace:

'; + echo '
' . htmlspecialchars($exception->getTraceAsString()) . '
'; + } + } else { + echo '

Internal Server Error

'; + echo '

An error occurred while processing your request.

'; + } + } +} \ No newline at end of file diff --git a/Engine/Error/ShiftError.php b/Engine/Error/ShiftError.php index c4ad1b3..803dcf7 100644 --- a/Engine/Error/ShiftError.php +++ b/Engine/Error/ShiftError.php @@ -18,18 +18,48 @@ */ class ShiftError extends \Error { + private string $customFile = ''; + private int $customLine = 0; /** * ShiftError constructor. * @param string $message * @param int $code * @param Throwable|null $previous + * @return void */ - public function __construct(string $message = "", int $code = 0, Throwable $previous = null) + public function __construct(string $message = "", int $code = 0, ?Throwable $previous = null) { parent::__construct($message, $code, $previous); - $line = (int)$this->getLine(); - $errorHighlighter = new ErrorHighlighter($this->getFile(), $line); + $this->renderError(); + } + + public function setFile(string $file): void + { + $this->customFile = $file; + } + + public function setLine(int $line): void + { + $this->customLine = $line; + } + + public function getCustomFile(): string + { + return $this->customFile ?: $this->getFile(); + } + + public function getCustomLine(): int + { + return $this->customLine ?: $this->getLine(); + } + + private function renderError(): void + { + $line = (int)$this->getCustomLine(); + $file = $this->getCustomFile(); + + $errorHighlighter = new ErrorHighlighter($file, $line); $highlighted = $errorHighlighter->highlighted; $stackTrace = $errorHighlighter->getBeautyStackTrace($this->getTrace()); @@ -56,18 +86,18 @@ public function __construct(string $message = "", int $code = 0, Throwable $prev
' . $this->getMessage() . '
-
' . $this->getFile() . ': ' . $line . '
+
' . $file . ': ' . $line . '
' . $highlighted . $stackTrace->traceItemsCode . '

Stack trace:
- + @@ -78,17 +108,17 @@ public function __construct(string $message = "", int $code = 0, Throwable $prev '; } - } diff --git a/Engine/Error/ShiftError/ErrorHighlighter.php b/Engine/Error/ShiftError/ErrorHighlighter.php index 8bd076f..ae10a0a 100644 --- a/Engine/Error/ShiftError/ErrorHighlighter.php +++ b/Engine/Error/ShiftError/ErrorHighlighter.php @@ -12,13 +12,13 @@ final class ErrorHighlighter { - public $highlighted = ''; + public string $highlighted = ''; /* * @var StackTrace */ - public function __construct(string $file, int $lineWithError = null, bool $hidden = false) + public function __construct(string $file, ?int $lineWithError = null, bool $hidden = false) { $this->setCodeStyle(); $this->highlighted = $this->highlight_file_with_line_numbers($file, $lineWithError, $hidden); @@ -35,11 +35,11 @@ protected final function setCodeStyle(): void /** * @param string $file - * @param int $lineWithError + * @param int|null $lineWithError * @param bool $hidden * @return string */ - protected final function highlight_file_with_line_numbers(string $file, int $lineWithError = null, bool $hidden = false): string + protected final function highlight_file_with_line_numbers(string $file, ?int $lineWithError = null, bool $hidden = false): string { $code = substr(highlight_file($file, true), 36, -15); $lines = explode('
', $code); diff --git a/Engine/Error/StorageError.php b/Engine/Error/StorageError.php index c8e9e27..b8f8b4c 100644 --- a/Engine/Error/StorageError.php +++ b/Engine/Error/StorageError.php @@ -23,6 +23,7 @@ class StorageError extends ShiftError * @param string $message * @param int $code * @param Throwable|null $previous + * @return void */ public function __construct(string $message = "", int $code = 0, Throwable $previous = null) { diff --git a/Engine/Request.php b/Engine/Request.php index 0fc0e2a..a77d9f8 100755 --- a/Engine/Request.php +++ b/Engine/Request.php @@ -8,138 +8,101 @@ */ class Request { - - /** - * @var - */ - protected static $array; - - /** - * @var - */ - protected static $controller; - - /** - * @var - */ - protected static $action; - - /** - * @var - */ - protected static $arguments = []; - - /** - * @var - */ - protected static $path; - - /** - * - */ - public static function setup(): void + private string $path; + private string $controller; + private string $action; + private array $arguments = []; + private array $queryParams = []; + private array $postData = []; + private array $serverData = []; + + public function __construct() { + $this->serverData = $_SERVER; + $this->queryParams = $_GET; + $this->postData = $_POST; + $this->parseRequest(); + } - self::$path = $_SERVER['REQUEST_URI']; - - $get = $_GET; - $post = $_POST; - - if (strpos(self::$path, '?') !== false) { - $arr = explode('?', self::$path); - self::$path = $arr[0]; + private function parseRequest(): void + { + $this->path = $this->serverData['REQUEST_URI'] ?? '/'; + + // Remove query string from path + if (strpos($this->path, '?') !== false) { + $parts = explode('?', $this->path); + $this->path = $parts[0]; } - self::$array = explode('/', trim(self::$path, '/')); - - self::$controller = (self::$array[0] ?? App::$defaultController) ?: App::$defaultController; - self::$action = (self::$array[1] ?? App::$defaultAction) ?: App::$defaultAction; - - if (count(self::$array) > 2) { - $tmp = array_slice(self::$array, 2, count(self::$array)); - self::$arguments = $tmp; + $pathSegments = explode('/', trim($this->path, '/')); + + $this->controller = $pathSegments[0] ?? 'index'; + $this->action = $pathSegments[1] ?? 'index'; + + // Extract arguments from path segments + if (count($pathSegments) > 2) { + $this->arguments = array_slice($pathSegments, 2); } + } + + public function getPath(): string + { + return $this->path; + } -// dd(Request::getArray(), Request::getArguments() , 'sdad', $get, $post); + public function getController(): string + { + return $this->controller; } - /** - * @return array - */ - public static function getArray(): array + public function getAction(): string { - return self::$array; + return $this->action; } - /** - * @param array $array - */ - public static function setArray(array $array): void + public function getArguments(): array { - self::$array = $array; + return $this->arguments; } - /** - * @return string - */ - public static function getController(): string + public function getQueryParams(): array { - return self::$controller; + return $this->queryParams; } - /** - * @param string $controller - */ - public static function setController(string $controller): void + public function getPostData(): array { - self::$controller = $controller; + return $this->postData; } - /** - * @return string - */ - public static function getAction(): string + public function getMethod(): string { - return self::$action; + return $this->serverData['REQUEST_METHOD'] ?? 'GET'; } - /** - * @param string $action - */ - public static function setAction($action): void + public function isPost(): bool { - self::$action = $action; + return $this->getMethod() === 'POST'; } - /** - * @return array - */ - public static function getArguments(): array + public function isGet(): bool { - return self::$arguments; + return $this->getMethod() === 'GET'; } - /** - * @param string $arguments - */ - public static function setArguments($arguments): void + public function getHeader(string $name): ?string { - self::$arguments = $arguments; + $headerKey = 'HTTP_' . strtoupper(str_replace('-', '_', $name)); + return $this->serverData[$headerKey] ?? null; } - /** - * @return string - */ - public static function getPath(): string + public function getUserAgent(): ?string { - return self::$path; + return $this->serverData['HTTP_USER_AGENT'] ?? null; } - /** - * @param string $path - */ - public static function setPath($path): void + public function getIpAddress(): ?string { - self::$path = $path; + return $this->serverData['REMOTE_ADDR'] ?? null; } } diff --git a/Engine/ServiceContainer.php b/Engine/ServiceContainer.php new file mode 100644 index 0000000..91ff3e1 --- /dev/null +++ b/Engine/ServiceContainer.php @@ -0,0 +1,110 @@ +services[$name] = $service; + } + + /** + * Register a singleton service + */ + public function singleton(string $name, $service): void + { + $this->singletons[$name] = $service; + } + + /** + * Resolve a service + */ + public function resolve(string $name) + { + // Check if already resolved + if (isset($this->resolved[$name])) { + return $this->resolved[$name]; + } + + // Check singletons first + if (isset($this->singletons[$name])) { + $service = $this->singletons[$name]; + $instance = $this->createInstance($service); + $this->resolved[$name] = $instance; + return $instance; + } + + // Check regular services + if (isset($this->services[$name])) { + $service = $this->services[$name]; + return $this->createInstance($service); + } + + throw new InvalidArgumentException("Service '{$name}' not found"); + } + + /** + * Check if service exists + */ + public function has(string $name): bool + { + return isset($this->services[$name]) || isset($this->singletons[$name]); + } + + /** + * Create instance from service definition + */ + private function createInstance($service) + { + if ($service instanceof Closure) { + return $service($this); + } + + if (is_string($service) && class_exists($service)) { + return new $service(); + } + + return $service; + } + + /** + * Clear all services + */ + public function clear(): void + { + $this->services = []; + $this->singletons = []; + $this->resolved = []; + } + + /** + * Get all registered service names + */ + public function getRegisteredServices(): array + { + return array_keys($this->services); + } + + /** + * Get all singleton service names + */ + public function getSingletonServices(): array + { + return array_keys($this->singletons); + } +} \ No newline at end of file diff --git a/Engine/ServiceInterface.php b/Engine/ServiceInterface.php new file mode 100644 index 0000000..ab55e72 --- /dev/null +++ b/Engine/ServiceInterface.php @@ -0,0 +1,25 @@ +storageDir = $storageParentDir . '/storage/'; - $this->storageViewsDir = $this->storageDir . '/views/'; - $this->checkDir($this->storageViewsDir); + $this->initializePaths(); + $this->ensureDirectoriesExist(); + } + + private function initializePaths(): void + { + $storageParentDir = dirname(__DIR__) . '/'; + $this->storageDir = $storageParentDir . 'storage/'; + $this->storageViewsDir = $this->storageDir . 'views/'; + } + + private function ensureDirectoriesExist(): void + { + $this->createDirectoryIfNotExists($this->storageDir); + $this->createDirectoryIfNotExists($this->storageViewsDir); } - private function checkDir(string $dir): void + /** + * @throws StorageError + */ + private function createDirectoryIfNotExists(string $dir): void { - if (!file_exists($dir)) { - mkdir($dir, 0777, true); + if (file_exists($dir)) { + $this->validateDirectory($dir); + return; } - if (!file_exists($dir)) { - throw new StorageError('Directory ' . $dir . ' does not exists'); + + if (!mkdir($dir, 0755, true)) { + throw new StorageError("Failed to create directory: {$dir}"); } + + $this->validateDirectory($dir); + } + + /** + * @throws StorageError + */ + private function validateDirectory(string $dir): void + { + if (!is_dir($dir)) { + throw new StorageError("Path '{$dir}' is not a directory"); + } + if (!is_writable($dir)) { - throw new StorageError('Directory ' . $dir . ' is not writable'); + throw new StorageError("Directory '{$dir}' is not writable"); } } + /** + * @throws StorageError + */ public function saveView(string $name, string $content): void { - if (!file_exists($this->storageViewsDir)) { - mkdir($this->storageViewsDir, 0777, true); + $filePath = $this->storageViewsDir . $name; + + if (file_put_contents($filePath, $content) === false) { + throw new StorageError("Failed to save view file: {$filePath}"); } + } + + public function getStorageDir(): string + { + return $this->storageDir; + } - file_put_contents($this->storageViewsDir . $name, $content); + public function getStorageViewsDir(): string + { + return $this->storageViewsDir; + } + + public function clearViews(): void + { + $files = glob($this->storageViewsDir . '*.php'); + foreach ($files as $file) { + if (is_file($file)) { + unlink($file); + } + } } } diff --git a/Engine/View.php b/Engine/View.php index 2dfc139..15ab3f6 100755 --- a/Engine/View.php +++ b/Engine/View.php @@ -19,67 +19,97 @@ */ class View { + private string $title = ''; + private array $scripts = []; + private array $styles = []; + private Storage $storage; + private string $templatePath; + private string $viewPath; + + public function __construct() + { + $this->storage = new Storage(); + $this->templatePath = __DIR__ . '/../application/view/template/index.php'; + $this->viewPath = __DIR__ . '/../application/view/controller/'; + } /** - * @var string - */ - private $_title = ''; - private $_scripts = []; - private $_styles = []; - - - /** - * @param null|string $view + * @param string|null $view * @param array $data * @param string $title * @param array $styles * @param array $scripts + * @return void */ - public function make(?string $view, array $data = [], string $title = '', array $styles = [], array $scripts = []) + public function make(?string $view, array $data = [], string $title = '', array $styles = [], array $scripts = []): void { $this->setTitle($title); $this->setScripts($scripts); $this->setStyles($styles); - if (!$view || strlen($view) === 0) { - $view = 'default'; - } - - $storage = new Storage(); + $viewName = $this->resolveViewName($view); + $viewContent = $this->loadViewContent($viewName); + $templateContent = $this->loadTemplateContent(); + + $fullView = $this->buildFullView($templateContent, $viewContent); + $this->renderView($fullView, $viewName, $data); + } - if (!file_exists(__DIR__ . '/../application/view/template/index.php')) { - throw new ShiftError('Template ' . $view . ' does not exists'); + private function resolveViewName(?string $view): string + { + if (!$view || $view === 'default') { + return Request::getController() . '/' . Request::getAction(); } - $template = file_get_contents(__DIR__ . '/../application/view/template/index.php'); + return $view; + } - if ($view === 'default') { - $view = Request::getController() . '/' . Request::getAction(); + /** + * @throws ShiftError + */ + private function loadViewContent(string $viewName): string + { + $viewFile = $this->viewPath . $viewName . '.php'; + + if (!file_exists($viewFile)) { + throw new ShiftError("View '{$viewName}' does not exist at path: {$viewFile}"); } + + return file_get_contents($viewFile); + } - if (!file_exists(__DIR__ . '/../application/view/controller/' . $view . '.php')) { - throw new ShiftError('View ' . $view . ' does not exists'); + /** + * @throws ShiftError + */ + private function loadTemplateContent(): string + { + if (!file_exists($this->templatePath)) { + throw new ShiftError("Template does not exist at path: {$this->templatePath}"); } - $viewContent = file_get_contents(__DIR__ . '/../application/view/controller/' . $view . '.php'); - - $viewName = md5($view) . '.php'; + + return file_get_contents($this->templatePath); + } - $fullView = str_replace('{{ $view }}', $viewContent, $template); + private function buildFullView(string $template, string $viewContent): string + { + return str_replace('{{ $view }}', $viewContent, $template); + } + private function renderView(string $fullView, string $viewName, array $data): void + { $builder = new ViewBuilder($fullView); - $builder - ->setScripts($this->_scripts) - ->setStyles($this->_styles) - ->setTitle($this->_title) + ->setScripts($this->scripts) + ->setStyles($this->styles) + ->setTitle($this->title) ->build(); - $storage->saveView( - $viewName, - $builder->getView() - ); - - require_once $storage->storageViewsDir . $viewName; + $viewFileName = md5($viewName) . '.php'; + $this->storage->saveView($viewFileName, $builder->getView()); + // Extract variables for view + extract($data); + + require_once $this->storage->storageViewsDir . $viewFileName; } /** @@ -87,7 +117,7 @@ public function make(?string $view, array $data = [], string $title = '', array */ public function getTitle(): string { - return $this->_title; + return $this->title; } /** @@ -95,7 +125,7 @@ public function getTitle(): string */ public function setTitle(string $title): void { - $this->_title = $title; + $this->title = $title; } /** @@ -103,7 +133,7 @@ public function setTitle(string $title): void */ public function getScripts(): array { - return $this->_scripts; + return $this->scripts; } /** @@ -111,7 +141,7 @@ public function getScripts(): array */ public function setScripts(array $scripts): void { - $this->_scripts = $scripts; + $this->scripts = $scripts; } /** @@ -119,7 +149,7 @@ public function setScripts(array $scripts): void */ public function getStyles(): array { - return $this->_styles; + return $this->styles; } /** @@ -127,6 +157,6 @@ public function getStyles(): array */ public function setStyles(array $styles): void { - $this->_styles = $styles; + $this->styles = $styles; } } diff --git a/Engine/View/ViewBuilder.php b/Engine/View/ViewBuilder.php index fd2ea68..faf3eb1 100755 --- a/Engine/View/ViewBuilder.php +++ b/Engine/View/ViewBuilder.php @@ -21,15 +21,15 @@ class ViewBuilder /** * @var string */ - private $html; + private string $html; /** * @var array */ - private $styles = []; + private array $styles = []; /** * @var array */ - private $scripts = []; + private array $scripts = []; /** * ViewBuilder constructor. @@ -71,9 +71,9 @@ public function build(): self } /** - * @return ViewBuilder + * @return void */ - private function buildStyles(): self + private function buildStyles(): void { $styleDestination = '/public/css/' . Request::getController() . '/' . Request::getAction() . '.css'; $styles = ''; @@ -89,13 +89,12 @@ private function buildStyles(): self } $this->html = str_replace('{{ $styles }}', $styles, $this->html); - return $this; } /** - * @return ViewBuilder + * @return void */ - private function buildScripts(): self + private function buildScripts(): void { $scriptsDestination = '/public/js/' . Request::getController() . '/' . Request::getAction() . '.js'; $scripts = ''; @@ -110,7 +109,6 @@ private function buildScripts(): self } } $this->html = str_replace('{{ $scripts }}', $scripts, $this->html); - return $this; } /** @@ -142,25 +140,14 @@ private function replacePHPCode(): void */ private function compileStatement(array $match): string { - switch ($match[1]) { - case 'include': - return ''; - break; - case 'if': - return ''; - break; - case 'for': - return ''; - break; - case 'foreach': - return ''; - break; - case 'while': - return ''; - break; - default: - return ''; - } + return match ($match[1]) { + 'include' => '', + 'if' => '', + 'for' => '', + 'foreach' => '', + 'while' => '', + default => '', + }; } /** @@ -169,8 +156,8 @@ private function compileStatement(array $match): string private function replacePHPVariables(): void { $pattern = '/{{ \$(\w+) }}/'; - $replcement = ''; - $this->html = preg_replace($pattern, $replcement, $this->html); + $replacement = ''; + $this->html = preg_replace($pattern, $replacement, $this->html); } /** diff --git a/REFACTORING.md b/REFACTORING.md new file mode 100644 index 0000000..164b850 --- /dev/null +++ b/REFACTORING.md @@ -0,0 +1,216 @@ +# ShiftPHP Framework - Refaktoryzacja + +## 🎯 **Przegląd zmian** + +Ten dokument opisuje kompleksową refaktoryzację frameworka ShiftPHP, która wprowadza nowoczesne wzorce projektowe, poprawia architekturę i zwiększa możliwości testowania. + +## 📋 **Lista zmian** + +### 1. **Refaktoryzacja klasy Request** +- **Przed**: Statyczne metody i właściwości +- **Po**: Instancje klas z dependency injection +- **Korzyści**: + - Łatwiejsze testowanie + - Lepsze zarządzanie stanem + - Dodatkowe metody (isPost, isGet, getHeader, etc.) + +### 2. **Refaktoryzacja klasy App** +- **Przed**: Statyczne metody, globalny stan +- **Po**: Instancje z dependency injection +- **Korzyści**: + - Kontrola cyklu życia aplikacji + - Możliwość testowania + - Lepsze zarządzanie błędami + +### 3. **Ulepszenie systemu widoków** +- **Przed**: Monolityczna klasa View +- **Po**: Podzielona na mniejsze, wyspecjalizowane metody +- **Korzyści**: + - Lepsze separation of concerns + - Łatwiejsze utrzymanie + - Czytelniejszy kod + +### 4. **Poprawa klasy Storage** +- **Przed**: Podstawowa walidacja, publiczne właściwości +- **Po**: Zaawansowana walidacja, enkapsulacja +- **Korzyści**: + - Bezpieczniejsze operacje na plikach + - Lepsze zarządzanie błędami + - Dodatkowe funkcje (clearViews) + +### 5. **Nowy system obsługi błędów** +- **Nowe**: Klasa ErrorHandler z centralnym zarządzaniem +- **Korzyści**: + - Spójna obsługa błędów + - Wsparcie dla CLI i web + - Możliwość customizacji + +### 6. **Service Container** +- **Nowe**: System dependency injection +- **Korzyści**: + - Loose coupling + - Łatwiejsze testowanie + - Zarządzanie zależnościami + +## 🔧 **Szczegóły techniczne** + +### Dependency Injection + +```php +// Przed +$app = new App(); +App::start(); + +// Po +$request = new Request(); +$app = new App($request); +$app->start(); +``` + +### Service Container + +```php +// Rejestracja serwisów +$app->getContainer()->singleton('database', function() { + return new Database(); +}); + +// Resolwowanie serwisów +$database = $app->resolve('database'); +``` + +### Obsługa błędów + +```php +// Automatyczna rejestracja w bootstrap.php +\Engine\Error\ErrorHandler::register(); + +// Custom handler +\Engine\Error\ErrorHandler::setCustomHandler(function($exception) { + // Custom error handling +}); +``` + +## 📁 **Struktura plików** + +``` +Engine/ +├── App.php # Główna klasa aplikacji (refaktoryzowana) +├── Request.php # Obsługa żądań (refaktoryzowana) +├── Controller.php # Bazowa klasa kontrolera (ulepszona) +├── View.php # System widoków (refaktoryzowany) +├── ServiceContainer.php # NOWY: Container dla DI +├── ServiceInterface.php # NOWY: Interfejs dla serwisów +├── Error/ +│ ├── ErrorHandler.php # NOWY: Centralny system błędów +│ └── ShiftError.php # Ulepszona obsługa błędów +└── Utils/ + └── Storage.php # Ulepszona klasa Storage +``` + +## 🚀 **Korzyści z refaktoryzacji** + +### 1. **Testowalność** +- Wszystkie klasy można teraz łatwo testować +- Dependency injection umożliwia mockowanie +- Brak globalnego stanu + +### 2. **Maintainability** +- Czytelniejszy kod +- Lepsze separation of concerns +- Mniejsze klasy z jedną odpowiedzialnością + +### 3. **Extensibility** +- Service container umożliwia łatwe dodawanie nowych serwisów +- Modularna architektura +- Interfejsy dla rozszerzeń + +### 4. **Error Handling** +- Centralny system obsługi błędów +- Lepsze logowanie +- Wsparcie dla różnych środowisk + +## 🔄 **Migracja** + +### Dla istniejących kontrolerów: + +```php +// Przed +class MyController extends \Engine\Controller +{ + public function index() + { + $this->render('view', ['data' => 'value']); + } +} + +// Po (bez zmian w API!) +class MyController extends \Engine\Controller +{ + public function index() + { + $this->render('view', ['data' => 'value']); + // Dodatkowo dostępne: + $this->json(['status' => 'success']); + $this->redirect('/home'); + } +} +``` + +### Dla punktu wejścia: + +```php +// Przed +require_once 'bootstrap.php'; +Engine\App::start(); + +// Po +require_once 'bootstrap.php'; +$request = new Engine\Request(); +$app = new Engine\App($request); +$app->start(); +``` + +## 🧪 **Testowanie** + +Framework jest teraz w pełni testowalny: + +```php +// Przykład testu +$request = $this->createMock(Engine\Request::class); +$request->method('getController')->willReturn('test'); +$request->method('getAction')->willReturn('index'); + +$app = new Engine\App($request); +// Test aplikacji... +``` + +## 📈 **Wydajność** + +- Brak statycznych właściwości = lepsze zarządzanie pamięcią +- Service container z singletonami = optymalizacja zasobów +- Lepsze zarządzanie błędami = szybsze debugowanie + +## 🔮 **Przyszłe rozszerzenia** + +Refaktoryzacja przygotowuje framework na: + +1. **Middleware system** +2. **Event system** +3. **Database abstraction layer** +4. **Caching system** +5. **CLI commands** +6. **API routing** + +## 📝 **Podsumowanie** + +Refaktoryzacja ShiftPHP wprowadza: + +- ✅ **Dependency Injection** +- ✅ **Service Container** +- ✅ **Lepsze zarządzanie błędami** +- ✅ **Testowalność** +- ✅ **Maintainability** +- ✅ **Extensibility** + +Framework jest teraz gotowy na przyszłe rozszerzenia i łatwiejszy w utrzymaniu. \ No newline at end of file diff --git a/application/controller/HelloController.php b/application/controller/HelloController.php index 297bb99..5dc2ef6 100755 --- a/application/controller/HelloController.php +++ b/application/controller/HelloController.php @@ -8,9 +8,32 @@ */ class HelloController extends \Engine\Controller { - public function index(): void { - $this->render('default', ['dd' => 'asd'], 'ssadfg'); + $this->render('default', [ + 'message' => 'Hello from ShiftPHP!', + 'timestamp' => date('Y-m-d H:i:s') + ], 'Welcome to ShiftPHP'); + } + + public function about(): void + { + $this->render('hello/about', [ + 'title' => 'About ShiftPHP', + 'version' => '1.0.0' + ], 'About - ShiftPHP'); + } + + public function api(): void + { + $this->json([ + 'status' => 'success', + 'message' => 'API endpoint working!', + 'data' => [ + 'controller' => $this->request->getController(), + 'action' => $this->request->getAction(), + 'arguments' => $this->request->getArguments() + ] + ]); } } diff --git a/application/view/controller/hello/index.php b/application/view/controller/hello/index.php index 5e46d0e..5a6c9aa 100644 --- a/application/view/controller/hello/index.php +++ b/application/view/controller/hello/index.php @@ -11,3 +11,5 @@ {{ $i }} @endfor + +{{ $dd }} diff --git a/bootstrap.php b/bootstrap.php index e0081b7..8282e3a 100755 --- a/bootstrap.php +++ b/bootstrap.php @@ -1,7 +1,22 @@ =7.1", - "grabower/clitypo": "dev-master", + "php": ">=8.3", "ext-json": "*" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/adriangrabowski/clitypo.git" - } - ], "autoload": { "psr-4": { "View\\": "Engine/View", diff --git a/index.php b/index.php index 0424831..b5b55a3 100644 --- a/index.php +++ b/index.php @@ -4,8 +4,9 @@ ini_set('display_errors', 'Off'); require_once 'bootstrap.php'; -require_once 'Engine/App.php'; -Engine\App::setHelpers(); -Engine\App::start(); +// Create request instance and start application +$request = new Engine\Request(); +$app = new Engine\App($request); +$app->start(); diff --git a/shift.php b/shift.php index 3f904d3..5493e67 100755 --- a/shift.php +++ b/shift.php @@ -1,6 +1,10 @@ #!/usr/bin/php run();
' . $line . ' - ' . str_replace(APP_ROOT . '/', '', $this->getFile()) . ' + ' . str_replace(APP_ROOT . '/', '', $file) . '