From 254cc88a422b084da2863fa2976c88956761088c Mon Sep 17 00:00:00 2001 From: Robert Date: Tue, 23 Jun 2026 18:27:02 -0400 Subject: [PATCH] ... --- src/Framework/Common.php | 7 +- src/Framework/Dollars.php | 31 --- src/Framework/Http/App/App.php | 100 +++++--- src/Framework/Http/App/AppHandler.php | 31 ++- src/Framework/Http/App/MiddlewareHandler.php | 31 +++ .../Http/App/MiddlewareInterface.php | 21 ++ src/Framework/Http/Kernel.php | 12 +- src/Framework/MoneyFormatter.php | 236 ++++++++++++++++++ src/Framework/View.php | 16 +- 9 files changed, 391 insertions(+), 94 deletions(-) delete mode 100644 src/Framework/Dollars.php create mode 100644 src/Framework/Http/App/MiddlewareHandler.php create mode 100644 src/Framework/Http/App/MiddlewareInterface.php create mode 100644 src/Framework/MoneyFormatter.php diff --git a/src/Framework/Common.php b/src/Framework/Common.php index 20e539e..b2f2aa6 100644 --- a/src/Framework/Common.php +++ b/src/Framework/Common.php @@ -126,11 +126,14 @@ final class Common return $combined; } - public static function doesAllOfTheArrayHaveData(array $a, array $skipKeys = []): false|array + public static function doesAllOfTheArrayHaveData( + array $a, array $skipKeys = [], + bool $failOnEmpty = false + ): false|array { if (self::isArrayWithData($a)) { foreach ($a as $key => $value) { - if ($value === false) { + if ($value === false || ($failOnEmpty === true && empty($value))) { if ( self::isArrayWithData($skipKeys) && in_array($key, $skipKeys) ) { diff --git a/src/Framework/Dollars.php b/src/Framework/Dollars.php deleted file mode 100644 index d72c6a2..0000000 --- a/src/Framework/Dollars.php +++ /dev/null @@ -1,31 +0,0 @@ -moneyAsFloat($money), 2, '.', ','); - } - - public function intl_USD(Number $money): string|false { - if (class_exists('NumberFormatter')) { - $formatter = new \NumberFormatter('en_US', \NumberFormatter::CURRENCY); - return $formatter->formatCurrency($this->moneyAsFloat($money), 'USD'); - } - return false; - } -} diff --git a/src/Framework/Http/App/App.php b/src/Framework/Http/App/App.php index 375da10..115ebbb 100644 --- a/src/Framework/Http/App/App.php +++ b/src/Framework/Http/App/App.php @@ -13,6 +13,7 @@ namespace IOcornerstone\Framework\Http\App; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use IOcornerstone\Framework\Http\{ + App\MiddlewareHandler, Contract\MiddlewareAwareInterface, Request, HttpFactory @@ -27,7 +28,7 @@ use IOcornerstone\Framework\{ }; use Exception; -class App implements MiddlewareAwareInterface +class App implements MiddlewareAwareInterface { private $file; @@ -39,7 +40,10 @@ class App implements MiddlewareAwareInterface private bool $testing = false; private string $dirClass = ""; // Testing, CLI, or empty - public function __construct() {} + public function __construct() + { + + } public function handle(ServerRequestInterface $request): ResponseInterface { @@ -48,16 +52,16 @@ class App implements MiddlewareAwareInterface if ($ret === false) { return $this->local404(); } - + // Make sure $ret is a ResponseInterface if (!$ret instanceof ResponseInterface) { // You might need to convert it, for example: - return (new HttpFactory())->createResponse(200, [], $ret); + return (new HttpFactory())->createResponse(body: $ret); } - + return $ret; } - + public function getMiddleware(): array { return $this->middleWare; @@ -94,7 +98,7 @@ class App implements MiddlewareAwareInterface /** * Do not declare a return type here, as it will Error out!! */ - private function startUp() + private function startUp(): mixed { $full_route = $this->request->getUri()->getPath(); $noFileExt = $this->GetWithoutFileExtension($full_route); @@ -170,14 +174,14 @@ class App implements MiddlewareAwareInterface if ($dir === false) { return false; } - } - + } + $file = Requires::saferFileExists(basename($safeFile) . 'Controller.php', $dir); if ($file === false || $file === null) { return false; } $class = Security::filterClass($safeFolders) . "\\" . Security::filterClass($safeFile . "Controller"); - + if (empty(trim($method))) { $method = ""; // Clear out null if exists } @@ -200,6 +204,47 @@ class App implements MiddlewareAwareInterface return (new HttpFactory())->createResponse(404, [], $myView); } + private function RunIt(object $ctrl, string $method, $params): mixed + { + try { + if (! method_exists($ctrl, $method)) { + return false; + } + + $reflection = new \ReflectionMethod($ctrl, $method); + + if ($reflection->isPrivate()) { + return false; + } + if ($reflection->isProtected()) { + return false; + } + if ($reflection->isPublic()) { + return $this->doMiddleware($ctrl, $method, $params); + } + } catch (\ReflectionException $e) { + return false; + } + } + + private function doMiddleware($appController, string $method, $params) + { + $middleware = $this->getMiddleware(); + + // Does it NOT contain an array of middleware? + if (! Common::isArrayWithData($middleware)) { + return $appController->$method($params); + } + + $handler = new MiddlewareHandler(); + + return $handler->run( + $appController, + $middleware, + fn ($appController) => $appController->$method($params) + ); + } + /** * Do controller action * @param string $file @@ -207,7 +252,7 @@ class App implements MiddlewareAwareInterface * @param string $method * @retval type */ - private function action(string $file, string $class, string $method, $params) + private function action(string $file, string $class, string $method, $params): mixed { $safer_file = Requires::saferFileExists($file); if (!$safer_file) { @@ -217,41 +262,24 @@ class App implements MiddlewareAwareInterface return false; } - $use_api = false; // Misc::isApi(); - $callClass = "\\Project\\" . $this->dirClass . "Controllers\\" . $class; $controller = new $callClass($this->request); - // Collect controller-level middleware Directly from the controller file, IE: public static array $middleware = [ \Project\classes\auth_middleware::class ]; - $this->middleWare = $controller::$middleware ?? []; + // Collect controller-level middleware Directly from the controller file, IE: public static array $middleware = [ AuthMiddleware::class ]; + $this->middleWare = $controller::$middleware ?? []; if ($method === "error" && str_contains($class, "App") && - method_exists($controller, $method) === false + method_exists($controller, $method) === false ) { //Broken_error(); return false; } - - if ($use_api) { - if (empty($method)) { - $method = "index"; - } - $method .= "_api"; - if (method_exists($controller, $method)) { - return $controller->$method($params); - } else { - return false; - } - } else { - if (!empty($method) && method_exists($controller, $method)) { - return $controller->$method($params); - } else { - if (empty($method) && method_exists($controller, 'index')) { - return $controller->index($params); - } - } + + if (empty($method)) { + $method = "Index"; } - return false; + + return $this->RunIt($controller, $method, $params); } } diff --git a/src/Framework/Http/App/AppHandler.php b/src/Framework/Http/App/AppHandler.php index d84cd9b..78af386 100644 --- a/src/Framework/Http/App/AppHandler.php +++ b/src/Framework/Http/App/AppHandler.php @@ -27,18 +27,27 @@ final class AppHandler implements RequestHandlerInterface { $result = $this->app->handle($request); - // Convert to ResponseInterface if needed + if ($result === false) { + return new Response( + body: Stream::fromString('404 Not Found'), + status: 404 + ); + } + + if (is_array($result)) { + return new Response( + body: $result, + headers: ['Content-Type' => 'application/json'] + ); + } + if ($result instanceof ResponseInterface) { return $result; } - - // If it returns false (for 404), create a response - if ($result === false) { - return new Response('404 Not Found', 404); - } - - $stream = Stream::fromString($result); - // If it returns something else, wrap it - return new Response(body: $result); + + return new Response( + body: Stream::fromString((string)$result) + ); } -} + +} \ No newline at end of file diff --git a/src/Framework/Http/App/MiddlewareHandler.php b/src/Framework/Http/App/MiddlewareHandler.php new file mode 100644 index 0000000..a4b5a39 --- /dev/null +++ b/src/Framework/Http/App/MiddlewareHandler.php @@ -0,0 +1,31 @@ +handle($request, $next); + }; + }, + $destination + ); + + return $pipeline($request); + } +} \ No newline at end of file diff --git a/src/Framework/Http/App/MiddlewareInterface.php b/src/Framework/Http/App/MiddlewareInterface.php new file mode 100644 index 0000000..6dc1ee8 --- /dev/null +++ b/src/Framework/Http/App/MiddlewareInterface.php @@ -0,0 +1,21 @@ +getMessage()); } } - + private function buildKernel() { $router = new Router(); @@ -82,13 +82,7 @@ class Kernel { // Combine global middleware $globalMiddleware = $this->getGlobalMiddleware(); - - // Add app-specific middleware if any - $appMiddleware = $app->getMiddleware(); - if (!empty($appMiddleware)) { - array_splice($globalMiddleware, -1, 0, $appMiddleware); - } - + return new MiddlewareQueueHandler( $globalMiddleware, $routingHandler diff --git a/src/Framework/MoneyFormatter.php b/src/Framework/MoneyFormatter.php new file mode 100644 index 0000000..1bb93a7 --- /dev/null +++ b/src/Framework/MoneyFormatter.php @@ -0,0 +1,236 @@ + ['symbol' => '$', 'locale' => 'en_US', 'name' => 'US Dollar'], + 'CAD' => ['symbol' => '$', 'locale' => 'en_CA', 'name' => 'Canadian Dollar'], + 'EUR' => ['symbol' => '€', 'locale' => 'de_DE', 'name' => 'Euro'], + 'GBP' => ['symbol' => '£', 'locale' => 'en_GB', 'name' => 'British Pound'], + 'JPY' => ['symbol' => '¥', 'locale' => 'ja_JP', 'name' => 'Japanese Yen'], + 'CNY' => ['symbol' => '¥', 'locale' => 'zh_CN', 'name' => 'Chinese Yuan'], + 'AUD' => ['symbol' => '$', 'locale' => 'en_AU', 'name' => 'Australian Dollar'], + 'NZD' => ['symbol' => '$', 'locale' => 'en_NZ', 'name' => 'New Zealand Dollar'], + 'CHF' => ['symbol' => 'CHF','locale' => 'de_CH', 'name' => 'Swiss Franc'], + 'SEK' => ['symbol' => 'kr', 'locale' => 'sv_SE', 'name' => 'Swedish Krona'], + 'NOK' => ['symbol' => 'kr', 'locale' => 'nb_NO', 'name' => 'Norwegian Krone'], + 'DKK' => ['symbol' => 'kr', 'locale' => 'da_DK', 'name' => 'Danish Krone'], + 'PLN' => ['symbol' => 'zł', 'locale' => 'pl_PL', 'name' => 'Polish Zloty'], + 'CZK' => ['symbol' => 'Kč', 'locale' => 'cs_CZ', 'name' => 'Czech Koruna'], + 'HUF' => ['symbol' => 'Ft', 'locale' => 'hu_HU', 'name' => 'Hungarian Forint'], + 'RON' => ['symbol' => 'lei','locale' => 'ro_RO', 'name' => 'Romanian Leu'], + 'BGN' => ['symbol' => 'лв', 'locale' => 'bg_BG', 'name' => 'Bulgarian Lev'], + 'TRY' => ['symbol' => '₺', 'locale' => 'tr_TR', 'name' => 'Turkish Lira'], + 'RUB' => ['symbol' => '₽', 'locale' => 'ru_RU', 'name' => 'Russian Ruble'], + 'UAH' => ['symbol' => '₴', 'locale' => 'uk_UA', 'name' => 'Ukrainian Hryvnia'], + 'INR' => ['symbol' => '₹', 'locale' => 'hi_IN', 'name' => 'Indian Rupee'], + 'PKR' => ['symbol' => '₨', 'locale' => 'ur_PK', 'name' => 'Pakistani Rupee'], + 'BDT' => ['symbol' => '৳', 'locale' => 'bn_BD', 'name' => 'Bangladeshi Taka'], + 'LKR' => ['symbol' => 'Rs', 'locale' => 'si_LK', 'name' => 'Sri Lankan Rupee'], + 'NPR' => ['symbol' => '₨', 'locale' => 'ne_NP', 'name' => 'Nepalese Rupee'], + 'THB' => ['symbol' => '฿', 'locale' => 'th_TH', 'name' => 'Thai Baht'], + 'VND' => ['symbol' => '₫', 'locale' => 'vi_VN', 'name' => 'Vietnamese Dong'], + 'MYR' => ['symbol' => 'RM', 'locale' => 'ms_MY', 'name' => 'Malaysian Ringgit'], + 'SGD' => ['symbol' => '$', 'locale' => 'en_SG', 'name' => 'Singapore Dollar'], + 'IDR' => ['symbol' => 'Rp', 'locale' => 'id_ID', 'name' => 'Indonesian Rupiah'], + 'PHP' => ['symbol' => '₱', 'locale' => 'en_PH', 'name' => 'Philippine Peso'], + 'KRW' => ['symbol' => '₩', 'locale' => 'ko_KR', 'name' => 'South Korean Won'], + 'HKD' => ['symbol' => '$', 'locale' => 'zh_HK', 'name' => 'Hong Kong Dollar'], + 'TWD' => ['symbol' => 'NT$','locale' => 'zh_TW', 'name' => 'New Taiwan Dollar'], + 'AED' => ['symbol' => 'د.إ','locale' => 'ar_AE', 'name' => 'UAE Dirham'], + 'SAR' => ['symbol' => '﷼', 'locale' => 'ar_SA', 'name' => 'Saudi Riyal'], + 'QAR' => ['symbol' => '﷼', 'locale' => 'ar_QA', 'name' => 'Qatari Riyal'], + 'KWD' => ['symbol' => 'د.ك','locale' => 'ar_KW', 'name' => 'Kuwaiti Dinar'], + 'BHD' => ['symbol' => '.د.ب','locale' => 'ar_BH', 'name' => 'Bahraini Dinar'], + 'OMR' => ['symbol' => '﷼', 'locale' => 'ar_OM', 'name' => 'Omani Rial'], + 'ILS' => ['symbol' => '₪', 'locale' => 'he_IL', 'name' => 'Israeli New Shekel'], + 'ZAR' => ['symbol' => 'R', 'locale' => 'en_ZA', 'name' => 'South African Rand'], + 'NGN' => ['symbol' => '₦', 'locale' => 'en_NG', 'name' => 'Nigerian Naira'], + 'EGP' => ['symbol' => 'E£', 'locale' => 'ar_EG', 'name' => 'Egyptian Pound'], + 'KES' => ['symbol' => 'KSh','locale' => 'sw_KE', 'name' => 'Kenyan Shilling'], + 'GHS' => ['symbol' => '₵', 'locale' => 'en_GH', 'name' => 'Ghanaian Cedi'], + 'BRL' => ['symbol' => 'R$', 'locale' => 'pt_BR', 'name' => 'Brazilian Real'], + 'ARS' => ['symbol' => '$', 'locale' => 'es_AR', 'name' => 'Argentine Peso'], + 'CLP' => ['symbol' => '$', 'locale' => 'es_CL', 'name' => 'Chilean Peso'], + 'COP' => ['symbol' => '$', 'locale' => 'es_CO', 'name' => 'Colombian Peso'], + 'MXN' => ['symbol' => '$', 'locale' => 'es_MX', 'name' => 'Mexican Peso'], + 'PEN' => ['symbol' => 'S/', 'locale' => 'es_PE', 'name' => 'Peruvian Sol'], + 'UYU' => ['symbol' => '$U', 'locale' => 'es_UY', 'name' => 'Uruguayan Peso'], +]; + + /** + * SQL for money: + * balance BIGINT NOT NULL DEFAULT 0, + * currency CHAR(3) NOT NULL DEFAULT 'USD' + */ + + public static function getBalance( + float $input, + string $currency = 'USD', + string $locale = 'en_US' + ): string + { + $locale = self::getLocaleFromCode($currency, $locale); + + $formatter = new NumberFormatter( + $locale, + NumberFormatter::CURRENCY + ); + + $amount = $formatter->formatCurrency( + abs($input), + $currency + ); + + if ($input < 0.00) { + return "-{$amount}"; + } + + if ($input > 0.01) { + return "+{$amount}"; + } + + return $amount; + } + + public static function getColorClass(float $input): string + { + if ($input < 0.00) { + return " money-negitive "; + } else if ($input > 0.01) { + return " money-positive "; + } else { + return " money-zero "; + } + } + + /** + * To store into the Database as whole integer + * This is For Migrations ONLY!!! + * Please use parseInputToMinorUnits on USER INPUT. + */ + public static function amountToMinorUnits( + float $amount, + string $currency = 'USD', + string $locale = 'en_US' + ): int + { + $locale = self::getLocaleFromCode($currency, $locale); + + $formatter = new NumberFormatter( + $locale, + NumberFormatter::CURRENCY + ); + + $formatter->setTextAttribute( + NumberFormatter::CURRENCY_CODE, + $currency + ); + + $digits = $formatter->getAttribute( + NumberFormatter::FRACTION_DIGITS + ); + + return (int) round( $amount * (10 ** $digits)); + } + + /** + * To get back from the Database into Decimal Numbers for Output + */ + public static function minorUnitsToAmount( + int $amount, + string $currency = 'USD', + string $locale = 'en_US' + ): float + { + $locale = self::getLocaleFromCode($currency, $locale); + + $formatter = new NumberFormatter( + $locale, + NumberFormatter::CURRENCY + ); + + $formatter->setTextAttribute( + NumberFormatter::CURRENCY_CODE, + $currency + ); + + $digits = $formatter->getAttribute( + NumberFormatter::FRACTION_DIGITS + ); + + return $amount / (10 ** $digits); + } + + /** + * Parse User Input Safely, to be saved into Database + */ + public static function parseInputToMinorUnits( + string $input, + string $currency = 'USD', + string $locale = 'en_US' + ): int + { + $locale = self::getLocaleFromCode($currency, $locale); + + return self::amountToMinorUnits((float) $input, $currency, $locale); + } + + public static function convertToUsd( + int $minorUnits, + float $exchangeRate, + string $currency, + ): int { + return (int) round($minorUnits * $exchangeRate); + } + + public static function getCurrencyCodes(string $unit = "USD"): string + { + $ret = ""; + foreach (self::CURRENCIES as $code => $currency) { + $ret .= sprintf( + '', + $code, + ($code === $unit) ? "selected" : "", + htmlspecialchars($currency['name']), + $code + ); + } + return $ret; + } + + public static function getLocaleFromCode(string $currencyCode = "USD", string $locale = "en_US"): ?string + { + if ($currencyCode === "USD" && $locale === "en_US") { + return $locale; + } + if ($currencyCode !== "USD" && ! empty($locale) && $locale !== "en_US") { + return $locale; + } + foreach (self::CURRENCIES as $code => $currency) { + if ($code === $currencyCode) { + return $currency['locale'] ?? null; + } + } + return null; + } +} diff --git a/src/Framework/View.php b/src/Framework/View.php index d8149e0..2eaa976 100644 --- a/src/Framework/View.php +++ b/src/Framework/View.php @@ -20,7 +20,7 @@ use IOcornerstone\Framework\{ final class View { - + const POST_VIEW_FILE_NAME = "View"; // Add this to the end of view file public bool $whiteSpaceControl = false; public string $pageOutput; private array $vars = []; @@ -102,9 +102,15 @@ final class View if ($viewFile === null) { return; } - + + $addPostFixedView = self::POST_VIEW_FILE_NAME; + $justFileName = pathinfo($viewFile, PATHINFO_FILENAME); + if (stripos($justFileName, self::POST_VIEW_FILE_NAME) !== false) { + $addPostFixedView = ""; // Give it an empty string as it already has the view in the FileName! + } + $fileExt = ViewType::getFileExtensionFor($type); - $file = $this->findViewPath($viewFile . "View" . $fileExt); + $file = $this->findViewPath($viewFile . $addPostFixedView . $fileExt); if ($type == ViewType::TWIG) { $this->useTemplateEngineTwig = true; $this->templateType = str_replace('.', '', $fileExt); @@ -122,8 +128,8 @@ final class View } if ($file === false) { - echo "No view file exists for: {$viewFile}View!"; - throw new \Exception("View File does not exist: " . $viewFile. "View"); + echo "No view file exists for: {$viewFile}{$addPostFixedView}!"; + throw new \Exception("View File does not exist: " . $viewFile . $addPostFixedView); } else { $this->files[] = array('file' => $file, 'path' => UseDir::PROJECT, 'fileType' => $fileExt); }