Repository: nezamy/route Branch: master Commit: cd683aef81c1 Files: 38 Total size: 87.1 KB Directory structure: gitextract_dakvb4o0/ ├── .gitignore ├── .php_cs ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src/ │ ├── DataType/ │ │ └── Uri.php │ ├── Http/ │ │ ├── Auth/ │ │ │ ├── AuthInterface.php │ │ │ ├── Basic.php │ │ │ └── Digest.php │ │ ├── Auth.php │ │ ├── GlobalRequest.php │ │ ├── Middleware.php │ │ ├── Request.php │ │ ├── Response.php │ │ └── Session.php │ ├── Prototype/ │ │ ├── ArrayPrototype.php │ │ ├── ConvertObject.php │ │ ├── Getter.php │ │ ├── GetterObject.php │ │ ├── ObjectStore.php │ │ ├── Setter.php │ │ └── StringPrototype.php │ ├── Route.php │ ├── Routing/ │ │ ├── Route.php │ │ ├── RouteHandler.php │ │ ├── RouteHandlerInterface.php │ │ ├── RouteParser.php │ │ ├── RouteParserInterface.php │ │ └── Router.php │ ├── Support/ │ │ ├── ArrayTrait.php │ │ └── Regex.php │ └── functions.php └── tests/ ├── AuthTest.php ├── DummyRequest.php ├── RouteParserTest.php ├── RouteTest.php └── bootstrap.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ Thumbs.db ehthumbs.db .DS_Store ._* .idea .idea/vcs.xml vendor .phpunit.* ================================================ FILE: .php_cs ================================================ @package Just EOF; return PhpCsFixer\Config::create() ->setRiskyAllowed(true) ->setRules([ '@PSR2' => true, '@Symfony' => true, '@DoctrineAnnotation' => true, '@PhpCsFixer' => true, 'header_comment' => [ 'commentType' => 'PHPDoc', 'header' => $header, 'separate' => 'none', 'location' => 'after_declare_strict', ], 'array_syntax' => [ 'syntax' => 'short' ], 'list_syntax' => [ 'syntax' => 'short' ], 'concat_space' => [ 'spacing' => 'one' ], 'blank_line_before_statement' => [ 'statements' => [ 'declare', ], ], 'ordered_imports' => [ 'imports_order' => [ 'class', 'function', 'const', ], 'sort_algorithm' => 'alpha', ], 'single_line_comment_style' => [ 'comment_types' => [ ], ], 'yoda_style' => [ 'always_move_variable' => false, 'equal' => false, 'identical' => false, ], 'phpdoc_align' => [ 'align' => 'left', ], 'multiline_whitespace_before_semicolons' => [ 'strategy' => 'no_multi_line', ], 'constant_case' => [ 'case' => 'lower', ], 'class_attributes_separation' => true, 'combine_consecutive_unsets' => true, 'declare_strict_types' => true, 'linebreak_after_opening_tag' => true, 'lowercase_static_reference' => true, 'no_useless_else' => true, 'no_unused_imports' => true, 'not_operator_with_successor_space' => true, 'not_operator_with_space' => false, 'ordered_class_elements' => true, 'php_unit_strict' => false, 'phpdoc_separation' => false, 'single_quote' => true, 'standardize_not_equals' => true, 'multiline_comment_opening_closing' => true, 'phpdoc_add_missing_param_annotation' => true, 'no_null_property_initialization' => true ]) ->setFinder( PhpCsFixer\Finder::create() ->exclude('public') ->exclude('runtime') ->exclude('vendor') ->in(__DIR__) ) ->setUsingCache(false); ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Mahmoud Elnezamy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Route v2.0 Route - Fast, flexible routing for PHP, enabling you to quickly and easily build RESTful web applications. ## Installation ```bash $ composer require nezamy/route ``` Or if you looking for ready template for using this route Go to https://github.com/nezamy/just Route requires PHP 7.4.0 or newer. ## Changes list - Rewrite route based on php 7.4 - Support Swoole extensions - Support locales to build multi languages website - Added Auth, Basic, Digest - Availability to customize route parser and handler - Smart dependency injection and service container ## Usage Only if using composer create index.php in root. Create an index.php file with the following contents: ```php set(Just\Http\Request::class, $request); container()->set(Just\Http\Response::class, $response); container()->set(Just\Routing\Router::class, $route); try { include 'app/routes.php'; $output = $route->run(); foreach ($output->headers->all() as $k => $v) { header("$k: $v"); } http_response_code($output->statusCode()); if ($output->hasRedirect()) { list($url, $code) = $output->getRedirect(); header("Location: $url", true, $code); } } catch (\Error $e) { pre($e, 'Error', 6); } catch (\Exception $e) { pre($e, 'Exception', 6); } echo response()->body(); ``` app/routes.php ```php set([ 'document_root' => '/var/www/public', 'enable_static_handler' => true, ]); $http->on("request", function (Request $request, Response $response) { $request = new Just\Http\Request( $request->header ?? [], $request->server ?? [], $request->cookie ?? [], $request->get ?? [], $request->post ?? [], $request->files ?? [], $request->tmpfiles ?? [] ); $response = new Just\Http\Response; $route = new Just\Routing\Router($request, $response); container()->set(Just\Http\Request::class, $request); container()->set(Just\Http\Response::class, $response); container()->set(Router::class, $route); try { include __DIR__ .'/app/routes.php'; $output = $route->run(); foreach ($output->headers->all() as $k => $v) { $response->header($k, $v); } $response->setStatusCode($output->statusCode()); if ($output->hasRedirect()) { list($url, $code) = $output->getRedirect(); $response->redirect($url, $code); } } catch (\Error $e) { pre($e, 'Error', 6); } catch (\Exception $e) { pre($e, 'Exception', 6); } $response->end(response()->body(true)); }); $http->start(); ``` ## How it works Routing is done by matching a URL pattern with a callback function. ### app/routes.php ```php Route::any('/', function() { return 'Hello World'; }); Route::post('/contact-us', function(\Just\Http\Request $req) { pre($req->body, 'Request'); }); ``` ### The callback can be any object that is callable. So you can use a regular function: ```php function pages() { return 'Page Content'; } Route::get('/', 'pages'); ``` ### Or a class method: ```php class home { public function pages() { return 'Home page Content'; } } Route::get('/', [home::class, 'pages']); // OR Route::get('/', 'home@pages'); ``` ## Method Routing ```php Route::any('/', function() {}); Route::get('/', function() {}); Route::post('/', function() {}); Route::put('/', function() {}); Route::patch('/', function() {}); Route::option('/', function() {}); Route::delete('/', function() {}); ``` ## Parameters ```php // This example will match any page name Route::get('/{page}', function($page) { return "you are in $page"; }); Route::get('/post/{id}', function($id) { // Will match anything like post/hello or post/5 ... // But not match /post/5/title return "post id $id"; }); // more than parameters Route::get('/post/{id}/{title}', function($id, $title) { return "post id $id and title $title"; }); // you can get parameter in any order Route::get('/post/{id}/{title}', function($title, $id) { return "post id $id and title $title"; }); ``` ### For “unlimited” optional parameters, you can do this: ```php // This example will match anything after blog/ - unlimited arguments Route::get('/blog/{any}:*', function($any) { pre($any); }); ``` ## Regular Expressions You can validate the args by regular expressions. ```php // Validate args by regular expressions uses :(your pattern here) Route::get('/{username}:([0-9a-z_.-]+)/post/{id}:([0-9]+)', function($username, $id) { return "author $username post id $id"; }); // You can add named regex pattern in routes Route::addPlaceholders([ 'username' => '([0-9a-z_.-]+)', 'id' => '([0-9]+)' ]); // Now you can use named regex Route::get('/{username}:username/post/{id}:id', function($username, $id) { return "author $username post id $id"; }); //if the parameter name match the placeholder name just ignore placeholder and route will deduct that Route::get('/{username}/post/{id}', function($username, $id) { return "author $username post id $id"; }); ``` ### Some named regex patterns already registered in routes ```php [ 'int' => '/([0-9]+)', 'multiInt' => '/([0-9,]+)', 'title' => '/([a-z_-]+)', 'key' => '/([a-z0-9_]+)', 'multiKey' => '/([a-z0-9_,]+)', 'isoCode2' => '/([a-z]{2})', 'isoCode3' => '/([a-z]{3})', 'multiIsoCode2' => '/([a-z,]{2,})', 'multiIsoCode3' => '/([a-z,]{3,})' ]; ``` ## Optional parameters You can specify named parameters that are optional for matching by adding (?) ```php Route::get('/post/{title}?:title/{date}?', function($title, $date) { $content = ''; if ($title) { $content = "

$title

"; }else{ $content = "

Posts List

"; } if ($date) { $content .= "Published $date"; } return $content; }); ``` ## Groups ```php Route::group('/admin', function() { // /admin/ Route::get('/', function() {}); // /admin/settings Route::get('/settings', function() {}); // nested group Route::group('/users', function() { // /admin/users Route::get('/', function() {}); // /admin/users/add Route::get('/add', function() {}); }); // Anything else Route::any('/{any}:*', function($any) { pre("Page ( $any ) Not Found", 6); }); }); ``` ### Groups with parameters ```php Route::group('/{module}', function($lang) { Route::post('/create', function() {}); Route::put('/update', function() {}); }); ``` ### Locales ```php // the first language is the default i.e. ar // when you hit the site http://localhost on the first time will redirect to http://localhost/ar Route::locale(['ar','en'], function(){ // will be /ar/ Route::get('/', function($locale){ //get current language pre($locale); }); // /ar/contact Route::get('/contact', function() {}); Route::group('/blog', function() { // /ar/blog/ Route::get('/', function() {}); }); }); // Also you can write locales like that or whatever you want Route::locale(['ar-eg','en-us'], function(){ // will be /ar/ Route::get('/', function($locale){ //get current language list($lang, $country) = explode('-', $locale, 2); pre("Lang is $lang, Country is $country"); }); }); ``` ### Auth #### Basic ```php $auth = new \Just\Http\Auth\Basic(['users' => [ 'user1' => '123456', 'user2' => '987654' ]]); Route::auth($auth, function (){ Route::get('/secret', function(\Just\Http\Request $req){ pre("Hello {$req->user()->get('username')}, this is a secret page"); }); }); ``` #### Digest ```php $auth = new \Just\Http\Auth\Digest(['users' => [ 'user1' => '123456', 'user2' => '987654' ]]); Route::auth($auth, function (){ Route::get('/secret', function(\Just\Http\Request $req){ pre("Hello {$req->user()->get('username')}, this is a secret page"); }); }); ``` ### Middleware #### Global ```php Route::use(function (\Just\Http\Request $req, $next){ //validate something the call next to continue or return whatever if you want break if($req->isMobile()){ return 'Please open from a desktop'; } return $next(); }, function ($next){ // another middleware $next(); }); // After Route::use(function ($next){ $response = $next(); // make some action return $response; }); ``` #### Middleware on groups ```php // if open from mobile device Route::middleware(fn(\Just\Http\Request $req, $next) => !$req->isMobile() ? '' : $next()) ->group('/mobile-only', function (){ Route::get('/', function(\Just\Http\Request $req){ pre($req->browser()); }); }); ``` If you make the middleware as a class, you can pass the class with namespace. the class should be had a `handle` method. ```php class MobileOnly{ public function handle(\Just\Http\Request $req, $next){ return !$req->isMobile() ? '' : $next(); } } Route::middleware(MobileOnly::class) ->group('/',function (){ Route::get('/', function(\Just\Http\Request $req){ pre($req->browser()); }); }); ``` #### Middleware on route ```php Route::get('/', function(\Just\Http\Request $req){ pre($req->browser()); })->middleware(MobileOnly::class); ``` ### Dependency injection To learn about Dependency injection and service container please visit this [link](https://github.com/nezamy/di) ### Handle and Parser customization Example of CustomRouteHandler ```php class CustomRouteHandler implements Just\Routing\RouteHandlerInterface { public function call(callable $handler, array $args = []) { return call_user_func_array($handler, $args); } public function parse($handler): callable { if (is_string($handler) && ! function_exists($handler)) { $handler = explode('@', $handler, 2); } return $handler; } } \Just\Route::setHandler(new CustomRouteHandler); ``` ```php class CustomRouteParser implements RouteParserInterface { public function parse(string $uri): array { $matchedParameter = []; $matchedPattern = []; $result = []; // parse uri here and return array of 3 elements // /{page} // /{page}? return ['parameters' => $matchedParameter, 'patterns' => $matchedPattern, 'result' => $result]; } } \Just\Route::setParser(new CustomRouteParser); ``` ================================================ FILE: composer.json ================================================ { "name": "nezamy/route", "type": "library", "description": "Route - Fast, flexible routing for PHP, enabling you to quickly and easily build RESTful web applications.", "keywords": [ "route", "restful", "api", "justframework" ], "homepage": "https://nezamy.com/route", "license": "MIT", "authors": [ { "name": "Mahmoud Elnezamy", "email": "mahmoud@nezamy.com", "homepage": "http://nezamy.com", "role": "Full Stack Web Developer" } ], "autoload": { "psr-4": { "Just\\": "src/", "": "" }, "files": [ "src/functions.php" ] }, "autoload-dev": { "psr-4": { "Just\\Test\\": "tests/" } }, "require": { "php": ">=7.4", "ext-json": "*", "ext-mbstring": "*", "nezamy/di": "^2.0" }, "require-dev": { "phpunit/phpunit": "^7.5 || ^8.5", "friendsofphp/php-cs-fixer": "^2.16" }, "scripts": { "fix": "php-cs-fixer fix ./src", "test": "phpunit" } } ================================================ FILE: phpunit.xml ================================================ ./tests/ app/ ================================================ FILE: src/DataType/Uri.php ================================================ * @package Just */ namespace Just\DataType; use Just\Prototype\StringPrototype; class Uri extends StringPrototype { public function getChunk(?string $uri = null) { $uri = $uri ?? $this->data; $chunk = $uri ? '/' . explode('/', $uri)[1] : '/'; return $this->isStatic($chunk) ? $chunk : '/*'; } public function isStatic(?string $uri = null) { $uri = $uri ?? $this->data; return ! $uri || $uri && strpbrk('*{}?:()', $uri) === false; } public function rtrim($char) { $this->data = rtrim($this->data, $char); } public function ltrim($char) { $this->data = ltrim($this->data, $char); } } ================================================ FILE: src/Http/Auth/AuthInterface.php ================================================ * @package Just */ namespace Just\Http\Auth; interface AuthInterface { public function check(): bool; public function validate(array $credentials): bool; public function redirectToLogin(); public function login(): bool; public function logout(): void; public function user(); } ================================================ FILE: src/Http/Auth/Basic.php ================================================ * @package Just */ namespace Just\Http\Auth; class Basic implements AuthInterface { public $error = ''; protected $options = []; private $user = []; public function __construct($options) { $this->options = array_merge([ 'users' => [], 'realm' => 'Restricted area', ], $options); } public function check(): bool { return $this->login(); } public function redirectToLogin() { $this->logout(); if (! $this->login()) { $this->logout(); response()->end($this->error); } } public function login(array $credentials = []): bool { $username = $password = null; if (request()->server->has('PHP_AUTH_USER') && request()->server->has('PHP_AUTH_PW')) { $username = request()->server->get('PHP_AUTH_USER'); $password = request()->server->get('PHP_AUTH_PW'); } elseif (request()->headers->has('authorization')) { $auth = request()->headers->get('authorization'); if (strpos(strtolower($auth), 'basic') === 0) { [$username, $password] = explode(':', base64_decode(substr($auth, 6))); } } return $this->validate(['username' => $username, 'password' => $password]); } public function logout(): void { response()->headers->set('WWW-Authenticate', 'Basic realm="' . $this->options['realm'] . '"'); response()->setStatusCode(401); } public function validate(array $credentials): bool { if ($credentials['username'] && $credentials['password']) { $username = filter_var($credentials['username'], FILTER_SANITIZE_STRING, FILTER_FLAG_ENCODE_HIGH | FILTER_FLAG_ENCODE_LOW); $password = filter_var($credentials['password'], FILTER_SANITIZE_STRING, FILTER_FLAG_ENCODE_HIGH | FILTER_FLAG_ENCODE_LOW); $users = $this->options['users']; if (isset($users[$username]) && $users[$username] == $password) { unset($credentials['password']); $this->user = $credentials; return true; } } $this->error = 'Authorization Required'; return false; } public function user(): array { return $this->user; } } ================================================ FILE: src/Http/Auth/Digest.php ================================================ * @package Just */ namespace Just\Http\Auth; class Digest implements AuthInterface { public $error = ''; protected $options = []; private $user = []; public function __construct($options) { $this->options = array_merge([ 'users' => [], 'realm' => 'Restricted area', 'qop' => 'auth', 'nonce' => uniqid(), 'error' => 'Wrong Credentials!', 'cancel' => 'Authorization Required', ], $options); } public function check(): bool { return $this->login(); } public function redirectToLogin() { $this->logout(); if (! $this->login()) { $this->logout(); response()->end($this->error); } } public function login(array $credentials = []): bool { $data = []; if (request()->server->has('PHP_AUTH_DIGEST')) { $data = $this->digest_parse(request()->server->get('PHP_AUTH_DIGEST')); } elseif (request()->headers->has('authorization')) { $data = $this->digest_parse(request()->headers->get('authorization')); } return $this->validate($data); } public function logout(): void { response()->headers->set('WWW-Authenticate', 'Digest realm="' . $this->options['realm'] . '",qop="' . $this->options['qop'] . '",nonce="' . $this->options['nonce'] . '",opaque="' . md5($this->options['realm']) . '"'); response()->setStatusCode(401); } public function validate(array $credentials): bool { if (! $credentials || ! isset($this->options['users'][$credentials['username']])) { $this->error = $this->options['error']; return false; } $A1 = md5($credentials['username'] . ':' . $this->options['realm'] . ':' . $this->options['users'][$credentials['username']]); $A2 = md5(request()->method() . ':' . $credentials['uri']); $valid_response = md5($A1 . ':' . $credentials['nonce'] . ':' . $credentials['nc'] . ':' . $credentials['cnonce'] . ':' . $credentials['qop'] . ':' . $A2); if ($credentials['response'] != $valid_response) { $this->error = $this->options['error']; return false; } return true; } public function user(): array { return $this->user; } private function digest_parse($txt) { // protect against missing data $needed_parts = ['nonce' => 1, 'nc' => 1, 'cnonce' => 1, 'qop' => 1, 'username' => 1, 'uri' => 1, 'response' => 1]; $data = []; $keys = implode('|', array_keys($needed_parts)); preg_match_all('@(' . $keys . ')=(?:([\'"])([^\2]+?)\2|([^\s,]+))@', $txt, $matches, PREG_SET_ORDER); foreach ($matches as $m) { $data[$m[1]] = $m[3] ? $m[3] : $m[4]; unset($needed_parts[$m[1]]); } return $needed_parts ? [] : $data; } } ================================================ FILE: src/Http/Auth.php ================================================ * @package Just */ namespace Just\Http; use Just\Http\Auth\AuthInterface; /** * Class Auth. * @method bool check() * @method bool validate(array $credentials) * @method void redirectToLogin() * @method bool login() * @method void logout() * @method array user() */ class Auth { private AuthInterface $auth; public function __construct(AuthInterface $auth) { $this->auth = $auth; if (! $auth->check()) { $auth->redirectToLogin(); } } public function __call($method, $args) { if (method_exists($this->auth, $method)) { return call_user_func_array([$this->auth, $method], $args); } throw new \LogicException("{$method} Not Exists on Just\\Http\\Auth"); } } ================================================ FILE: src/Http/GlobalRequest.php ================================================ * @package Just */ namespace Just\Http; class GlobalRequest extends Request { public function __construct() { $headers = []; $server = []; foreach ($_SERVER as $key => $value) { if (substr($key, 0, 5) === 'HTTP_') { $key = substr($key, 5); // $key = $this->parseHeaderKey($key); $headers[$key] = $value; continue; } $key = strtolower($key); $server[$key] = $value; } if (! isset($headers['authorization'])) { if (isset($_SERVER['REDIRECT_HTTP_AUTHORIZATION'])) { $headers['authorization'] = $_SERVER['REDIRECT_HTTP_AUTHORIZATION']; } elseif (isset($_SERVER['PHP_AUTH_USER'])) { $basic_pass = isset($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : ''; $headers['authorization'] = 'Basic ' . base64_encode($_SERVER['PHP_AUTH_USER'] . ':' . $basic_pass); } elseif (isset($_SERVER['PHP_AUTH_DIGEST'])) { $headers['authorization'] = $_SERVER['PHP_AUTH_DIGEST']; } } if (isset($server['content_type']) && $server['content_type'] == 'application/x-www-form-urlencoded') { parse_str(file_get_contents('php://input'), $input); } else { $input = json_decode(file_get_contents('php://input'), true); } $post = array_merge($input ?? [], $_POST); parent::__construct($headers, $server, $_COOKIE, $_GET, $post, $_FILES); } } ================================================ FILE: src/Http/Middleware.php ================================================ * @package Just */ namespace Just\Http; use Just\Routing\RouteHandlerInterface; class Middleware { private array $layers = []; private RouteHandlerInterface $handler; public function __construct(RouteHandlerInterface $handler) { $this->handler = $handler; } public function add(array $layers): void { $this->layers = array_merge($this->layers, $layers); } public function handle(callable $core) { $layers = array_reverse($this->layers); $final = array_reduce($layers, function ($nextLayer, $layer) { return $this->createLayer($nextLayer, $layer); }, function () use ($core) { return $this->handler->call($core); }); return $this->handler->call($final); } private function createLayer($nextLayer, $layer): callable { return function () use ($nextLayer, $layer) { container()->setVar('next', $nextLayer); if(is_string($layer) && class_exists($layer)){ $layer = [$layer, 'handle']; } $layer = $this->handler->parse($layer); return $this->handler->call($layer); }; } } ================================================ FILE: src/Http/Request.php ================================================ * @package Just */ namespace Just\Http; use Just\Prototype\GetterObject; /** * Class Request. * @method array arguments() * @method string uri() * @method string method() * @method string currentUrl() * @method string protocol() * @method string scheme() * @method array user() */ class Request { public GetterObject $headers; public GetterObject $server; public GetterObject $cookies; public GetterObject $query; public GetterObject $body; public GetterObject $files; public GetterObject $tmp_files; protected $_arguments = []; protected $_uri; protected $_method; protected $_url; protected $_currentUrl; protected $_protocol; protected $_scheme; protected GetterObject $_user; public function __construct(array $headers, array $server, array $cookies, array $query, array $body, array $files, $tmp_files = []) { $this->headers = new class($headers) extends GetterObject { protected function getterTransformKey($key): string { return str_replace([' ', '_'], '-', strtolower($key)); } }; $this->server = new class($server) extends GetterObject { protected function getterTransformKey($key): string { return strtolower($key); } }; $this->cookies = new GetterObject($cookies); $this->query = new GetterObject($query); $this->body = new GetterObject($body); $this->files = new GetterObject($files); $this->tmp_files = new GetterObject($tmp_files); $this->_protocol = isset($server['server_protocol']) ? str_replace('HTTP/', '', $server['server_protocol']) : '1.1'; $this->_scheme = isset($server['https']) && $server['https'] === 'on' || isset($headers['x-forwarded-proto']) && $headers['x-forwarded-proto'] == 'https' ? 'https' : 'http'; $this->_uri = urldecode(parse_url($server['request_uri'] ?? '', PHP_URL_PATH) ?? '') ?? '/'; $this->_url = $this->_scheme . '://' . $this->serverName(); $this->_currentUrl = $this->url(rtrim($this->_uri, '/') ?? ''); $this->_method = $server['request_method'] ?? 'GET'; } public function __call($method, $args) { if (isset($this->{'_' . $method})) { return $this->{'_' . $method}; } throw new \LogicException("{$method} Not Exists on Just\\Http\\Request"); } public function url($uri = ''): string { return $this->_url . $uri; } public function serverName(): string { return $this->server->get('server_name', $this->headers->get('host', 'localhost')); } public function setArguments(array $args): void { $this->_arguments = $args; } public function setUser(array $user): void { $this->_user = new GetterObject($user); } public function isJson(): bool { if ($this->headers->get('X_REQUESTED_WITH') == 'XMLHttpRequest' || strpos($this->headers->get('ACCEPT'), '/json') !== false) { return true; } return false; } public function ip(): string { if ($this->headers->has('client_ip')) { $ip = $this->headers->get('client_ip'); } elseif ($this->headers->has('x_forwarded_for')) { $ip = $this->headers->get('x_forwarded_for'); } elseif ($this->headers->has('x_forwarded')) { $ip = $this->headers->get('x_forwarded'); } elseif ($this->headers->has('forwarded_for')) { $ip = $this->headers->get('forwarded_for'); } elseif ($this->headers->has('forwarded')) { $ip = $this->headers->get('forwarded'); } elseif ($this->server->has('remote_addr')) { $ip = $this->server->get('remote_addr'); } else { $ip = getenv('REMOTE_ADDR'); } if (! filter_var($ip, FILTER_VALIDATE_IP)) { return 'unknown'; } return $ip; } public function browser(): string { $user_agent = $this->headers->get('user-agent'); if (strpos($user_agent, 'Opera') || strpos($user_agent, 'OPR/')) { return 'Opera'; } if (strpos($user_agent, 'Edge')) { return 'Edge'; } if (strpos($user_agent, 'Chrome')) { return 'Chrome'; } if (strpos($user_agent, 'Safari')) { return 'Safari'; } if (strpos($user_agent, 'Firefox')) { return 'Firefox'; } if (strpos($user_agent, 'MSIE') || strpos($user_agent, 'Trident/7')) { return 'Internet Explorer'; } return 'unknown'; } public function platform(): string { $user_agent = $this->headers->get('user-agent'); if (preg_match('/linux/i', $user_agent)) { return 'linux'; } if (preg_match('/macintosh|mac os x/i', $user_agent)) { return 'mac'; } if (preg_match('/windows|win32/i', $user_agent)) { return 'windows'; } return 'unknown'; } public function isMobile(): bool { $aMobileUA = [ '/iphone/i' => 'iPhone', '/ipod/i' => 'iPod', '/ipad/i' => 'iPad', '/android/i' => 'Android', '/blackberry/i' => 'BlackBerry', '/webos/i' => 'Mobile', ]; // Return true if mobile User Agent is detected. foreach ($aMobileUA as $sMobileKey => $sMobileOS) { if (preg_match($sMobileKey, $this->headers->get('user-agent'))) { return true; } } // Otherwise, return false. return false; } } ================================================ FILE: src/Http/Response.php ================================================ * @package Just */ namespace Just\Http; use Just\Prototype\ObjectStore; class Response { public $headers; protected $_body = ''; protected $_statusCode = 200; protected $_redirect = []; private $_end = false; public function __construct() { $this->headers = new ObjectStore(); } public function body($clean = false): string { $body = $this->_body; if ($clean) { $this->_body = ''; } return $body; } public function write($body): void { if (! $this->_end) { $this->_body .= $body; } } public function end(string $body = ''): void { if (! $this->_end) { $this->_body = $body; $this->_end = true; } } public function isEnded(): bool { return $this->_end; } public function setStatusCode(int $statusCode): void { $this->_statusCode = $statusCode; } public function statusCode(): int { return $this->_statusCode; } public function redirectTo(string $location, int $status_code = 302) { $this->_redirect = [$location, $status_code]; } public function hasRedirect(): bool { return count($this->_redirect) > 0; } public function getRedirect() { return $this->_redirect; } } ================================================ FILE: src/Http/Session.php ================================================ * @package Just */ namespace Just\Http; class Session { private $options = []; public function __construct(array $options) { $this->options = array_merge([ 'storage', ], $options); } } ================================================ FILE: src/Prototype/ArrayPrototype.php ================================================ * @package Just */ namespace Just\Prototype; class ArrayPrototype { use Getter; use ConvertObject; use Setter; public function __construct(array $data = []) { $this->replace($data); return $this; } // public function set(string $key, $value): void { // if(!$value instanceof StringPrototype){ // $type = gettype($value); // switch ($type) { // case 'string': // $value = new StringPrototype($value); // break; // case 'array': // case 'object': // $value = new ArrayPrototype((array)$value); // } // } // $this->_set($key, $value); // } } ================================================ FILE: src/Prototype/ConvertObject.php ================================================ * @package Just */ namespace Just\Prototype; use Just\Storage\KeyValue\KeyValueStoreInterface; trait ConvertObject { public function __toString(): string { return $this->toJson(); } public function toArray($data = null): array { $array = []; $data = $data ?? $this->all(); foreach ((array) $data as $k => $v) { if (is_object($v)) { if ($v instanceof StringPrototype) { $array[$k] = (string) $v; } else { $array[$k] = $v instanceof KeyValueStoreInterface ? $this->toArray($v->all()) : $v; } } else { $array[$k] = $v; } } return $array; } public function toJson(): string { return json_encode($this->toArray(), JSON_PRETTY_PRINT); } } ================================================ FILE: src/Prototype/Getter.php ================================================ * @package Just */ namespace Just\Prototype; use ArrayIterator; trait Getter { private $data; public function get(string $key, $default = '') { return $this->data[$this->getterTransformKey($key)] ?? $default; } public function has(string $key): bool { return isset($this->data[$this->getterTransformKey($key)]); } public function all(): array { return $this->data; } public function only(...$keys): array { $results = []; $keys = $this->variadic(...$keys); foreach ($keys as $key) { $results[$key] = $this->get($key); } return $results; } public function except(...$keys): array { $results = $this->data; $keys = $this->variadic(...$keys); foreach ($keys as $key) { unset($results[$key]); } return $results; } public function keys(): array { return array_keys($this->data); } public function count(): int { return count($this->data); } public function last() { return array_key_last($this->data); } public function getIterator(): ArrayIterator { return new ArrayIterator($this->data); } public function variadic(...$keys): array { if (is_array($keys[0])) { $shift = array_shift($keys); $keys = array_merge($shift, $keys); } return $keys; } protected function getterTransformKey($key): string { return $key; } } ================================================ FILE: src/Prototype/GetterObject.php ================================================ * @package Just */ namespace Just\Prototype; class GetterObject { use Getter; public function __construct(array $data = []) { foreach ($data as $k => $v) { $this->set($k, $v); } } private function set(string $key, $value): void { $this->data[$this->getterTransformKey($key)] = $value; } } ================================================ FILE: src/Prototype/ObjectStore.php ================================================ * @package Just */ namespace Just\Prototype; class ObjectStore { use Setter; use Getter; use ConvertObject; public function __construct(array $data = []) { $this->replace($data); } } ================================================ FILE: src/Prototype/Setter.php ================================================ * @package Just */ namespace Just\Prototype; trait Setter { private $data; public function set(string $key, $value): void { $this->data[$this->setterTransformKey($key)] = $this->setterTransformValue($value); } public function push(string $key, $value): void { $key = $this->setterTransformKey($key); if (! isset($this->data[$key])) { $this->data[$key] = []; } elseif (! is_array($this->data[$key])) { $this->data[$key] = (array) $this->data[$key]; } $this->data[$key][] = $this->setterTransformValue($value); } public function pop(string $key) { $key = $this->setterTransformKey($key); if (! isset($this->data[$key])) { throw new \LogicException(__CLASS__ . " | The value of [key] {$this->data[$key]} is not defined"); } if (! is_array($this->data[$key])) { throw new \LogicException(__CLASS__ . " | The value of [key] {$this->data[$key]} is not array"); } return array_pop($this->data[$key]); } public function increment(string $key, int $by = 1): int { $key = $this->setterTransformKey($key); if (isset($this->data[$key]) && (string) $this->data[$key] !== ((string) (int) $this->data[$key])) { throw new \LogicException(__CLASS__ . " | The value of [key] {$this->data[$key]} is not integer"); } return $this->data[$key] = (int) ($this->data[$key] ?? 0) + $by; } public function decrement(string $key, int $by = 1): int { $key = $this->setterTransformKey($key); if (isset($this->data[$key]) && (string) $this->data[$key] !== ((string) (int) $this->data[$key])) { throw new \LogicException(__CLASS__ . " | the value of [key] {$this->data[$key]} is not integer"); } return $this->data[$key] = (int) ($this->data[$key] ?? 0) - $by; } public function add(array $data): void { foreach ($data as $k => $v) { $this->set($k, $v); } } public function replace(array $data): void { $this->clear(); foreach ($data as $k => $v) { $this->set((string) $k, $v); } } public function remove(string $key): void { unset($this->data[$this->setterTransformKey($key)]); } public function clear(): void { $this->data = []; } protected function setterTransformKey($key): string { return $key; } protected function setterTransformValue($value) { return $value; } } ================================================ FILE: src/Prototype/StringPrototype.php ================================================ * @package Just */ namespace Just\Prototype; class StringPrototype { protected string $data; public function __construct(string $value) { $this->data = $value; } public function __toString(): string { return $this->data; } public function set($newValue) { $this->data = $newValue; } public function eq(string $string) { return $this->data === $string; } public function match(string $string, &$matches) { return preg_match($string, $this->data, $matches); } public function ends(string $string) { return substr($this->data, -strlen($string)) === $string; } public function startsWith(string $string) { return substr($this->data, 0, strlen($string)) === $string; } public function limit(int $limit, $trimMarker = '') { return mb_strimwidth($this->data, 0, $limit, $trimMarker); } /** * @return bool|int */ public function contain(string $string) { return strpos($this->data, $string); } /** * @return $this */ public function trim(string $character_mask = " \t\n\r\0\x0B") { $this->data = trim($this->data, $character_mask); return $this; } public function append($value): void { $this->data .= $value; } public function prepend($value): void { $this->data = $value . $this->data; } } ================================================ FILE: src/Route.php ================================================ * @package Just */ namespace Just; use Just\Http\Auth\AuthInterface; use Just\Routing\RouteHandlerInterface; use Just\Routing\RouteParserInterface; /** * Class App. * * @method static \Just\Routing\Route any(string $uri, mixed $handler) * @method static \Just\Routing\Route get(string $uri, mixed $handler) * @method static \Just\Routing\Route head(string $uri, mixed $handler) * @method static \Just\Routing\Route post(string $uri, mixed $handler) * @method static \Just\Routing\Route put(string $uri, mixed $handler) * @method static \Just\Routing\Route patch(string $uri, mixed $handler) * @method static \Just\Routing\Route options(string $uri, mixed $handler) * @method static \Just\Routing\Route delete(string $uri, mixed $handler) * @method static \Just\Routing\Router middleware(...$middleware) * @method static \Just\Routing\Route auth(AuthInterface $auth, callable $callback) * @method static \Just\Routing\Route locale(array $locales, callable $callback) * @method static void addPlaceholders(array $patterns) * @method static void setNotfound($handler) * @method static void group($prefix, callable $callback) * @method static void use(...$middleware) * @method static void setHandler(RouteHandlerInterface $handler) * @method static void setParser(RouteParserInterface $parser) * */ class Route { public static function __callStatic($method, $args) { $instance = container()->get(\Just\Routing\Router::class); return $instance->{$method}(...$args); } } ================================================ FILE: src/Routing/Route.php ================================================ * @package Just */ namespace Just\Routing; use Just\DataType\Uri; use Just\Http\Auth\AuthInterface; use Just\Prototype\ArrayPrototype; class Route { public ArrayPrototype $options; private string $method; private Uri $uri; /** * @var callable */ private $handler; private array $args = []; private array $middleware = []; public function __construct(string $method, Uri $uri, $handler, array $options = []) { $this->method = $method; $this->uri = $uri; $this->handler = $handler; $this->options = new ArrayPrototype($options); } public function getMethod(): string { return $this->method; } public function getUri(): Uri { return $this->uri; } // parse before get public function getHandler(RouteHandlerInterface $handler): callable { return $handler->parse($this->handler); } public function addNamespaceToHandler() { if (is_string($this->handler)) { $this->handler = $this->options->get('namespace') . '\\' . $this->handler; } elseif (is_array($this->handler)) { $this->handler[0] = $this->options->get('namespace') . '\\' . $this->handler[0]; } } public function getArgs(): array { return $this->args; } public function setArgs(array $args): void { $this->args = $args; } public function middleware(...$middleware) { $this->middleware = array_merge($this->middleware, $middleware); return $this; } public function getMiddleware() { return $this->middleware; } public function hasMiddleware() { return count($this->middleware) > 0; } public function withoutMiddleware(...$middleware) { $this->middleware = array_filter($this->middleware, fn ($var) => ! in_array($var, $middleware)); return $this; } } ================================================ FILE: src/Routing/RouteHandler.php ================================================ * @package Just */ namespace Just\Routing; use Just\DI\Resolver; use LogicException; class RouteHandler implements RouteHandlerInterface { private Resolver $resolver; public function __construct() { $this->resolver = new Resolver(); } public function call(callable $handler, array $args = []) { return $this->resolver->resolve($handler); } public function parse($handler): callable { if (is_string($handler) && ! function_exists($handler)) { $handler = str_replace('::', '@', $handler); $handler = explode('@', $handler, 2); } if (is_callable($handler)) { if (is_array($handler)) { $handler = $this->resolver->prepare($handler); } return $handler; } if (! is_callable($handler)) { $method = isset($handler[1]) ? '::' . $handler[1] : ''; throw new LogicException("[{$handler[0]}{$method}] is not callable"); } return $handler; } } ================================================ FILE: src/Routing/RouteHandlerInterface.php ================================================ * @package Just */ namespace Just\Routing; interface RouteHandlerInterface { public function call(callable $handler, array $args = []); public function parse($handler): callable; } ================================================ FILE: src/Routing/RouteParser.php ================================================ * @package Just */ namespace Just\Routing; use Just\Support\Regex; class RouteParser implements RouteParserInterface { public function parse(string $uri): array { $matchedParameter = []; $matchedPattern = []; $result = preg_replace_callback('/\/\{([a-z-0-9@]+)\}\??((:\(?[^\/]+\)?)?)/i', function ($match) use (&$matchedParameter, &$matchedPattern) { [$full, $parameter, $namedPattern] = $match; $pattern = '/' . Regex::get('?'); if (! empty($namedPattern)) { $replace = substr($namedPattern, 1); if (Regex::has($replace)) { $pattern = '/' . Regex::get($replace); } elseif (substr($replace, 0, 1) == '(' && substr($replace, -1, 1) == ')') { $pattern = '/' . $replace; } } elseif (Regex::has($parameter)) { $pattern = '/' . Regex::get($parameter); } // Check whether parameter is optional. if (strpos($full, '?') !== false) { $pattern = str_replace(['/(', '|'], ['(/', '|/'], $pattern) . '?'; } $matchedParameter[] = $parameter; $matchedPattern[] = $pattern; return $pattern; }, trim($uri)); return ['parameters' => $matchedParameter, 'patterns' => $matchedPattern, 'result' => $result]; } } ================================================ FILE: src/Routing/RouteParserInterface.php ================================================ * @package Just */ namespace Just\Routing; interface RouteParserInterface { /** * @param string $uri i.e '/{username}:([0-9a-z_.-]+)/post/{id}:([0-9]+)' * @return array [ * 'parameters' => ['username', 'id'], * 'patterns' => ['/([0-9a-z_.-]+)', '/([0-9]+)'], * 'result' => '/([0-9a-z_.-]+)/post/([0-9]+)' * ] */ public function parse(string $uri): array; } ================================================ FILE: src/Routing/Router.php ================================================ * @package Just */ namespace Just\Routing; use Just\DataType\Uri; use Just\Http\Auth; use Just\Http\Auth\AuthInterface; use Just\Http\Middleware; use Just\Http\Request; use Just\Http\Response; use Just\Prototype\ArrayPrototype; use Just\Support\Regex; /** * Class Route. * @method Route any(string $uri, mixed $handler, array $options = []) * @method Route get(string $uri, mixed $handler, array $options = []) * @method Route head(string $uri, mixed $handler, array $options = []) * @method Route post(string $uri, mixed $handler, array $options = []) * @method Route put(string $uri, mixed $handler, array $options = []) * @method Route patch(string $uri, mixed $handler, array $options = []) * @method Route options(string $uri, mixed $handler, array $options = []) * @method Route delete(string $uri, mixed $handler, array $options = []) */ class Router { protected array $allowedMethods = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE', 'ANY']; protected array $allowedContentType = ['html' => 'text/html', 'json' => 'application/json', 'jsonp' => 'application/javascript']; protected Route $matched; private Request $request; private Response $response; private RouteHandlerInterface $handler; private RouteParserInterface $parser; private array $middleware_list = []; private array $globalMiddleware = []; private array $routes = []; private string $currentGroupPrefix = ''; private array $currentGroupOptions = []; private array $nextGroupOptions = []; private array $groupOptions = []; private ?string $localeRedirectHandle = null; private array $auth_list = []; private ?string $currentAuthId = null; private \Closure $notfound; public function __construct(Request $request, Response $response) { $this->request = $request; $this->response = $response; $this->parser = new RouteParser(); $this->handler = new RouteHandler(); $this->notfound = function () { pre('Page is not found', '404 Not Found', 6); }; container()->set(Request::class, $this->request); container()->set(Response::class, $this->response); } public function __call($method, $args) { return call_user_func_array([$this, 'add'], array_merge([$method], $args)); } public function setHandler(RouteHandlerInterface $handler) { $this->handler = $handler; } public function setParser(RouteParserInterface $parser) { $this->parser = $parser; } public function setNotfound($handler) { $this->notfound = $handler; } public function run(): Response { $this->match($this->request->method(), $this->request->uri()); return $this->response; } public function add($method, $uri, $handler, array $options = []): Route { $uri = new Uri($this->currentGroupPrefix . $uri); $chunk = $uri->getChunk(); if (! isset($this->routes[$chunk])) { $this->routes[$chunk] = []; } if (! in_array($method = strtoupper($method), $this->allowedMethods)) { throw new \LogicException("[{$method}] Method is not allowed"); } if($method == 'ANY'){ $method = ''; } $opt = []; if ($this->currentAuthId) { $opt['auth'] = $this->currentAuthId; } if ($options) { $opt = array_merge($opt, $options); } if ($this->currentGroupOptions) { $opt['group'] = $this->currentGroupOptions; } $this->routes[$chunk][] = $route = new Route($method, $uri, $handler, $opt); return $route; } public function locale(array $locales, callable $callback): void { if (session_status() == PHP_SESSION_NONE) { @session_start(); } if (! isset($_SESSION['locale'])) { $_SESSION['locale'] = $locales[0]; } $this->group('/{@locale}?:(' . implode('|', $locales) . ')', $callback); } public function auth(AuthInterface $auth, callable $callback) { $id = uniqid((string) rand()); $this->auth_list[$id] = $auth; $this->currentAuthId = $id; $callback($this); $this->currentAuthId = ''; } public function group($prefix, callable $callback): void { $previousGroupOptions = $this->currentGroupOptions; if ($this->nextGroupOptions) { $id = uniqid((string) rand()); $this->groupOptions[$id] = $this->nextGroupOptions; $this->currentGroupOptions[] = $id; $this->nextGroupOptions = []; } $previousGroupPrefix = $this->currentGroupPrefix; $this->currentGroupPrefix = $previousGroupPrefix . $prefix; $callback($this); $this->currentGroupPrefix = $previousGroupPrefix; $this->currentGroupOptions = $previousGroupOptions; } public function middleware(...$middleware): Router { $id = uniqid((string) rand()); $this->middleware_list[$id] = $middleware; if (! isset($this->nextGroupOptions['middleware'])) { $this->nextGroupOptions['middleware'] = []; } $this->nextGroupOptions['middleware'][] = $id; return $this; } public function use(...$middleware): Router { $this->globalMiddleware = array_merge($this->globalMiddleware, $middleware); return $this; } public function addPlaceholders(array $patterns): void { foreach ($patterns as $name => $pattern) { Regex::set($name, $pattern); } } public function match(string $method, string $uri): bool { $uri = new Uri($uri); $uri->rtrim('/'); $chunk = $uri->getChunk(); $matched = null; if (isset($this->routes[$chunk])) { $matched = $this->find($method, $uri, $this->routes[$chunk]); } if (! $matched && isset($this->routes['/*'])) { $matched = $this->find($method, $uri, $this->routes['/*']); } if ($matched) { $this->handleOptions($matched); if ($this->response->isEnded()) { return false; } $args = $matched->getArgs(); if (count($args)) { container()->importVars($args); $this->request->setArguments($args); } $middleware = new Middleware($this->handler); $middleware->add($matched->getMiddleware()); $output = $middleware->handle( $matched->getHandler($this->handler) ); $this->response->write($output ?? ''); return true; } $this->handler->call($this->notfound); return false; } public function handleMiddleware(Route $matched) { if ($this->globalMiddleware) { foreach ($this->globalMiddleware as $item) { $matched->middleware($item); } } if ($matched->options->has('middleware')) { $middleware = (array) $matched->options->get('middleware'); foreach (array_reverse($middleware) as $item) { call_user_func_array([$matched, 'middleware'], $this->middleware_list[$item]); } } } public function getMatched() { return $this->matched; } public function export(): array { return $this->routes; } private function handleOptions(Route $matched) { if ($matched->options->has('auth')) { $this->handleAuth($matched->options->get('auth')); } if ($matched->options->has('group')) { $this->handleGroupOptions($matched->options); } $this->handleMiddleware($matched); if ($matched->options->has('namespace')) { $matched->addNamespaceToHandler(); } //Handle Json if ($matched->options->has('content_type') && isset($this->allowedContentType[$matched->options->get('content_type')])) { $this->response->headers->set('Content-Type', $this->allowedContentType[$matched->options->get('content_type')]); } } private function handleAuth(string $id) { if (! isset($this->auth_list[$id])) { throw new \LogicException('Auth is not Registered'); } $auth = new Auth($this->auth_list[$id]); $this->request->setUser($auth->user()); } private function handleGroupOptions(ArrayPrototype $options) { $final = []; foreach ($options->get('group') as $id) { foreach ($this->groupOptions[$id] as $key => $value) { if ($key == 'namespace') { $final['namespace'] = (isset($final['namespace']) ? $final['namespace'] . '\\' : '') . $value; continue; } if ($key == 'middleware') { $final['middleware'] = array_merge($final['middleware'] ?? [], $value); continue; } $final[$key] = $value; } } $options->remove('group'); $options->add($final); } private function find(string $requestMethod, Uri $requestUri, $routes): Route { $result_args = []; $matched = false; foreach ($routes as $route) { $uri = $route->getUri(); $method = $route->getMethod(); $uri->rtrim('/'); if ($method && $method !== $requestMethod) { continue; } if ($uri->isStatic() && $uri->eq((string) $requestUri)) { $matched = true; } else { $pattern = $this->parser->parse((string) $uri); if (preg_match('~^' . $pattern['result'] . '$~i', (string) $requestUri, $args)) { array_shift($args); if ($args) { $args = array_map(function ($s) { return ltrim($s, '/'); }, $args); } $result_args = $this->bindArgs($pattern['parameters'], $args); $matched = true; } } if ($matched) { $route->setArgs($result_args); $this->matched = $route; return $route; } } return new Route('', new Uri('/404'), $this->notfound); } private function bindArgs(array $pram, array $args): array { $newArgs = []; if (count($pram) == count($args)) { $pram = array_map(function ($s) { return ltrim($s, '@'); }, $pram); $newArgs = array_combine($pram, $args); } else { foreach ($pram as $p) { $value = array_shift($args); if ($p == '@locale') { $p = 'locale'; if (! $value) { $this->localeRedirect(); break; } } $newArgs[$p] = $value; } } if (isset($newArgs['locale'])) { $_SESSION['locale'] = $newArgs['locale']; } return $newArgs; } private function localeRedirect() { $value = $_SESSION['locale']; $this->response->redirectTo('/' . $value . $this->request->uri()); if ($this->localeRedirectHandle && is_callable($this->localeRedirectHandle)) { $redirect = call_user_func_array($this->localeRedirectHandle, [$this->response->getRedirect(), $this->request->uri()]); if ($redirect) { $this->response->redirectTo('/' . $value . $this->request->uri()); } } $this->response->end(); } } ================================================ FILE: src/Support/ArrayTrait.php ================================================ * @package Just */ namespace Just\Support; /** * ArrTrait. * * @since 1.0.0 */ trait ArrayTrait { /** * Get value from nested array. * * @param string $k * @param string $default * * @return mixed */ public static function get(array $arr, $k, $default = null) { if (isset($arr[$k])) { return $arr[$k]; } $nested = explode('.', $k); foreach ($nested as $part) { if (isset($arr[$part])) { $arr = $arr[$part]; continue; } $arr = $default; break; } return $arr; } /** * set value to nested array. * * @param string $k * @param mixed $v * * @return array */ public static function set(array $arr, $k, $v) { $nested = ! is_array($k) ? explode('.', $k) : $k; $count = count($nested); if ($count == 1) { return $arr[$k] = $v; } if ($count > 1) { $prev = ''; $loop = 1; $unshift = $nested; foreach ($nested as $part) { if (isset($arr[$part]) && $count > $loop) { $prev = $part; array_shift($unshift); ++$loop; continue; } if ($loop > 1 && $loop < $count) { if (! isset($arr[$prev][$part])) { $arr[$prev][$part] = []; } $arr[$prev] = static::set($arr[$prev], $unshift, $v); ++$loop; break; } if ($loop >= 1 && $loop == $count) { if (! is_array($arr[$prev])) { $arr[$prev] = []; } if ($part == '') { $arr[$prev][] = $v; } else { $arr[$prev][$part] = $v; } } else { $arr[$part] = []; $prev = $part; array_shift($unshift); ++$loop; } } } return $arr; } /** * Get value if key exists or default value. * * @param string $k * @param string $default * * @return mixed */ public static function value(array $arr, $k, $default = null) { return isset($arr[$k]) ? $arr[$k] : $default; } /** * Get value from string json. * * @param string $jsonStr * @param string $k * @param string $default * * @return mixed */ public static function json($jsonStr, $k = null, $default = null) { $json = json_decode($jsonStr, true); if ($k && $json) { return self::get($json, $k, $default); } return $json; } // public static function replace_values($arr, $from = null, $to = "") // { // $results = []; // foreach ($arr as $k => $v) { // $toArr = (array) $v; // if(count($toArr) > 1 || is_array($v)){ // $results[$k] = nullToString($toArr); // } else { // $results[$k] = $v; // if ($v == $from) { // $results[$k] = $to; // } // } // } // return $results; // } } ================================================ FILE: src/Support/Regex.php ================================================ * @package Just */ namespace Just\Support; /** * Class Regex. * @method static string get(string $name) * @method static boolean has(string $name) * @method static void set(string $name, string $pattern) * @method static void update(string $name, string $pattern) * @method static array list() */ class Regex { private static ?Regex $instance = null; private array $patterns = [ '*' => '(.*)', '?' => '([^\/]+)', 'int' => '([0-9]+)', 'multiInt' => '([0-9,]+)', 'title' => '([a-z_-]+)', 'key' => '([a-z0-9_]+)', 'multiKey' => '([a-z0-9_,]+)', 'isoCode2' => '([a-z]{2})', 'isoCode3' => '([a-z]{3})', ]; public function __call(string $name, array $arguments) { return call_user_func_array([$this, '_' . $name], $arguments); } public static function __callStatic(string $name, array $arguments) { return call_user_func_array([self::instance(), '_' . $name], $arguments); } public static function instance(): self { if (static::$instance === null) { static::$instance = new static(); } return static::$instance; } public function _set(string $name, string $pattern): void { if ($this->has($name)) { throw new \LogicException("{$name} already registered in route patterns"); } $this->patterns[$name] = $pattern; } public function _get(string $name): string { return $this->patterns[$name]; } public function _has(string $name): bool { return array_key_exists($name, $this->patterns); } public function _update(string $name, string $pattern): void { $this->patterns[$name] = $pattern; } public function _list(): array { return $this->patterns; } } ================================================ FILE: src/functions.php ================================================
write(ob_get_clean()); } function dpre() { call_user_func_array('pre', func_get_args()); response()->end(response()->body()); } function container() { return \Just\DI\Container::instance(); } function response(): Just\Http\Response { return container()->get(\Just\Http\Response::class); } function request(): Just\Http\Request { return container()->get(\Just\Http\Request::class); } function auth(): Just\Http\Auth { return container()->get(\Just\Http\Auth::class); } ================================================ FILE: tests/AuthTest.php ================================================ set(Request::class, new GlobalRequest); Container::instance()->set(Response::class, new Response); $credentials = ['users'=> ['Mahmoud'=> '123258']]; $auth = new Auth(new Auth\Basic($credentials)); $this->assertTrue($auth->validate(['username'=> 'Mahmoud', 'password' => '123258'])); // $auth-> $this->assertTrue(response()->headers->has('WWW-Authenticate')); } /** * Undocumented function * * @return void */ public function testDigest(): void { Container::instance()->set(Request::class, new GlobalRequest); Container::instance()->set(Response::class, new Response); new Auth(new Auth\Digest(['users'=> ['Mahmoud'=> '123258']])); $this->assertTrue(response()->headers->has('WWW-Authenticate')); } } ================================================ FILE: tests/DummyRequest.php ================================================ _uri = $uri; } public function setMethod(string $method) { $this->_method = $method; } } ================================================ FILE: tests/RouteParserTest.php ================================================ parse('/{username}/post/{id}'); $this->assertSame([ 'parameters' => [ 'username', 'id' ], 'patterns' => [ '/([^\/]+)', '/([^\/]+)' ], 'result' => '/([^\/]+)/post/([^\/]+)' ], $results); } public function testParametersWithRegex() : void { $parser = new RouteParser(); $results = $parser->parse('/{username}:([0-9a-z_.-]+)/post/{id}:([0-9]+)'); $this->assertSame([ 'parameters' => [ 'username', 'id' ], 'patterns' => [ '/([0-9a-z_.-]+)', '/([0-9]+)' ], 'result' => '/([0-9a-z_.-]+)/post/([0-9]+)' ], $results); } public function testParametersWithPlaceholder() : void { $parser = new RouteParser(); $results = $parser->parse('/{username}:title/post/{id}:int'); $this->assertSame([ 'parameters' => [ 'username', 'id' ], 'patterns' => [ '/([a-z_-]+)', '/([0-9]+)' ], 'result' => '/([a-z_-]+)/post/([0-9]+)' ], $results); } public function testParametersWithPlaceholderAll() : void { $parser = new RouteParser(); $results = $parser->parse('/{all}:*'); $this->assertSame([ 'parameters' => [ 'all' ], 'patterns' => [ '/(.*)', ], 'result' => '/(.*)' ], $results); } public function testParametersHasSamePlaceholderName() : void { $parser = new RouteParser(); $results = $parser->parse('/post/{title}'); $this->assertSame([ 'parameters' => [ 'title' ], 'patterns' => [ '/([a-z_-]+)', ], 'result' => '/post/([a-z_-]+)' ], $results); } public function testOptionalParametersWithPlaceholder() : void { $parser = new RouteParser(); $results = $parser->parse('/{username}:title/posts/{id}?:int'); $this->assertSame([ 'parameters' => [ 'username', 'id' ], 'patterns' => [ '/([a-z_-]+)', '(/[0-9]+)?' ], 'result' => '/([a-z_-]+)/posts(/[0-9]+)?' ], $results); } public function testLocaleParameter() : void { $parser = new RouteParser(); $results = $parser->parse('/{@locale}:(ar|en)/posts/{id}?:int'); $this->assertSame([ 'parameters' => [ '@locale', 'id' ], 'patterns' => [ '/(ar|en)', '(/[0-9]+)?' ], 'result' => '/(ar|en)/posts(/[0-9]+)?' ], $results); } public function testLocaleOptionalParameter() : void { $parser = new RouteParser(); $results = $parser->parse('/{@locale}?:(ar|en)/posts/{id}?:int'); $this->assertSame([ 'parameters' => [ '@locale', 'id' ], 'patterns' => [ '(/ar|/en)?', '(/[0-9]+)?' ], 'result' => '(/ar|/en)?/posts(/[0-9]+)?' ], $results); } } ================================================ FILE: tests/RouteTest.php ================================================ app(); $r->add('GET', '/get/users', 'Just\Test\controller::method'); $r->delete('/delete', 'Just\Test\controller::method'); $r->get('/get', 'Just\Test\controller::method'); $r->head('/head', 'Just\Test\controller::method'); $r->patch('/patch', 'Just\Test\controller::method'); $r->post('/post', 'Just\Test\controller::method'); $r->put('/put', 'Just\Test\controller::method'); $r->options('/options', 'Just\Test\controller::method'); $expected = [ '/get' => [ new Route('GET', new Uri('/get/users'), 'Just\Test\controller::method'), new Route('GET', new Uri('/get'), 'Just\Test\controller::method'), ], '/delete' => [ new Route('DELETE', new Uri('/delete'), 'Just\Test\controller::method') ], '/head' => [ new Route('HEAD', new Uri('/head'), 'Just\Test\controller::method') ], '/patch' => [ new Route('PATCH', new Uri('/patch'), 'Just\Test\controller::method') ], '/post' => [ new Route('POST', new Uri('/post'), 'Just\Test\controller::method') ], '/put' => [ new Route('PUT', new Uri('/put'), 'Just\Test\controller::method') ], '/options' => [ new Route('OPTIONS', new Uri('/options'), 'Just\Test\controller::method') ] ]; self::assertEquals($expected, $r->export()); } public function testGroups() : void { $r = $this->app(); $r->get('/get', 'Just\Test\controller::method'); $r->group('/api/v1', function (Router $r) { $r->get('/posts', 'Just\Test\controller::method'); $r->post('/posts/create', 'Just\Test\controller::method'); $r->group('/nested-group', function (Router $r) { $r->get('/posts', 'Just\Test\controller::method'); $r->post('/posts/create', 'Just\Test\controller::method'); }); }); $r->group('/admin', static function (Router $r): void { $r->get('-some-info', 'Just\Test\controller::method'); }); $r->group('/admin-', static function (Router $r): void { $r->get('more-info', 'Just\Test\controller::method'); }); $expected = [ '/get' => [ new Route('GET', new Uri('/get'), 'Just\Test\controller::method'), ], '/api' => [ new Route('GET', new Uri('/api/v1/posts'), 'Just\Test\controller::method'), new Route('POST', new Uri('/api/v1/posts/create'), 'Just\Test\controller::method'), new Route('GET', new Uri('/api/v1/nested-group/posts'), 'Just\Test\controller::method'), new Route('POST', new Uri('/api/v1/nested-group/posts/create'), 'Just\Test\controller::method'), ], '/admin-some-info' => [ new Route('GET', new Uri('/admin-some-info'), 'Just\Test\controller::method'), ], '/admin-more-info' => [ new Route('GET', new Uri('/admin-more-info'), 'Just\Test\controller::method'), ] ]; self::assertEquals($expected, $r->export()); } public function testRouteDynamic() : void { $r = $this->app(); $r->get('/{user}/{id}?', 'Just\Test\controller::method'); $r->get('/user/{user}/{id}?', 'Just\Test\controller::method'); self::assertEquals([ '/*' => [ new Route('GET', new Uri('/{user}/{id}?'), 'Just\Test\controller::method') ], '/user' => [ new Route('GET', new Uri('/user/{user}/{id}?'), 'Just\Test\controller::method') ], ], $r->export()); try { $r->match('GET', '/username'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{user}/{id}?'), 'Just\Test\controller::method'); $expect->setArgs([ 'user' => 'username', 'id' => null ]); self::assertEquals($expect, $r->getMatched()); try { $r->match('GET', '/username/10/'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{user}/{id}?'), 'Just\Test\controller::method'); $expect->setArgs([ 'user' => 'username', 'id' => 10 ]); self::assertEquals($expect, $r->getMatched()); } public function testGroupDynamic() : void { $r = $this->app(); $r->group('/{lang}:isoCode2', function (Router $r) { $r->get('/{page}', 'Just\Test\controller::method'); $r->get('/post/{post}', 'Just\Test\controller::method'); }); self::assertEquals([ '/*' => [ new Route('GET', new Uri('/{lang}:isoCode2/{page}'), 'Just\Test\controller::method'), new Route('GET', new Uri('/{lang}:isoCode2/post/{post}'), 'Just\Test\controller::method') ] ], $r->export()); try { $r->match('GET', '/ar/page'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{lang}:isoCode2/{page}'), 'Just\Test\controller::method'); $expect->setArgs([ 'lang' => 'ar', 'page' => 'page' ]); self::assertEquals($expect, $r->getMatched()); } public function testGroupDynamicOptionalParameter() : void { $r = $this->app(); $r->group('/{lang}?:isoCode2', function (Router $r) { $r->get('/{page}', 'Just\Test\controller::method'); $r->get('/post/{post}', 'Just\Test\controller::method'); }); self::assertEquals([ '/*' => [ new Route('GET', new Uri('/{lang}?:isoCode2/{page}'), 'Just\Test\controller::method'), new Route('GET', new Uri('/{lang}?:isoCode2/post/{post}'), 'Just\Test\controller::method') ] ], $r->export()); try { $r->match('GET', '/page'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{lang}?:isoCode2/{page}'), 'Just\Test\controller::method'); $expect->setArgs([ 'lang' => '', 'page' => 'page' ]); self::assertEquals($expect, $r->getMatched()); try { $r->match('GET', '/ar/page'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{lang}?:isoCode2/{page}'), 'Just\Test\controller::method'); $expect->setArgs([ 'lang' => 'ar', 'page' => 'page' ]); self::assertEquals($expect, $r->getMatched()); } public function testAddPlaceholder() : void { $r = $this->app(); $r->addPlaceholders([ 'test' => '(test_[a-z_-]+)' ]); $this->assertSame(Regex::get('test'), '(test_[a-z_-]+)'); } public function testLocale(): void { $r = $this->app(); $r->locale(['ar', 'en'], function (Router $r) { $r->get('/', 'Just\Test\controller::method'); $r->get('/page', 'Just\Test\controller::method'); $r->get('/page/{page}', 'Just\Test\controller::method'); }); self::assertEquals([ '/*' => [ new Route('GET', new Uri('/{@locale}?:(ar|en)/'), 'Just\Test\controller::method'), new Route('GET', new Uri('/{@locale}?:(ar|en)/page'), 'Just\Test\controller::method'), new Route('GET', new Uri('/{@locale}?:(ar|en)/page/{page}'), 'Just\Test\controller::method'), ] ], $r->export()); try { $r->match('GET', '/en/page'); } catch (\LogicException $e) { } $expect = new Route('GET', new Uri('/{@locale}?:(ar|en)/page'), 'Just\Test\controller::method'); $expect->setArgs([ 'locale' => 'en', ]); self::assertEquals($expect, $r->getMatched()); } public function testLocaleRedirect(): void { $req = new DummyRequest(); $req->setUri('/page'); $r = new Router($req, new Response()); $r->locale(['ar', 'en'], function (Router $r) { $r->get('/', function () { }); $r->get('/page', function () { }); }); $match = $r->run(); $this->assertTrue($match->hasRedirect()); $this->assertTrue($match->isEnded()); $this->assertEquals(['/ar/page', 302], $match->getRedirect()); } } ================================================ FILE: tests/bootstrap.php ================================================