From 186bea1e822ed88fb5423f391cf350746a2bc6c5 Mon Sep 17 00:00:00 2001 From: ABGEO Date: Thu, 7 Mar 2019 00:27:15 +0400 Subject: [PATCH 1/6] Add base functionality --- examples/src/config/routes.json | 7 +++++++ examples/src/controllers/DefaultController.php | 11 +++++++++++ examples/test.php | 5 ++++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 examples/src/config/routes.json create mode 100644 examples/src/controllers/DefaultController.php diff --git a/examples/src/config/routes.json b/examples/src/config/routes.json new file mode 100644 index 0000000..ef1c363 --- /dev/null +++ b/examples/src/config/routes.json @@ -0,0 +1,7 @@ +{ + "homepage": { + "path": "/", + "method": "GET", + "action": "Cherry\\DefaultController::index" + } +} \ No newline at end of file diff --git a/examples/src/controllers/DefaultController.php b/examples/src/controllers/DefaultController.php new file mode 100644 index 0000000..b36031b --- /dev/null +++ b/examples/src/controllers/DefaultController.php @@ -0,0 +1,11 @@ + Date: Thu, 7 Mar 2019 00:29:35 +0400 Subject: [PATCH 2/6] Add base functionality --- composer.json | 4 +- composer.lock | 53 +++++++++++++++-- src/Router.php | 155 ++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 205 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 86c56e3..fe72a9f 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,9 @@ ], "minimum-stability": "dev", "require": { - "php": ">=5.6.0" + "php": ">=5.6.0", + "cherry-project/request": "^1.0", + "ext-json": "*" }, "autoload": { "psr-4": { diff --git a/composer.lock b/composer.lock index 66fd42e..d1917e5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,9 +4,53 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "91b2ab3d450c1045ada04c044f6c007c", - "content-hash": "581e6dac62919bed4dffd93a9fae2879", - "packages": [], + "hash": "8b160ed158bc9317292ef41f624dc135", + "content-hash": "807be3d4f706dbd55d53b25fb1bf1137", + "packages": [ + { + "name": "cherry-project/request", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/ABGEO07/cherry-request.git", + "reference": "96df4a237b895d11866233c68fe53f1d64a68183" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ABGEO07/cherry-request/zipball/96df4a237b895d11866233c68fe53f1d64a68183", + "reference": "96df4a237b895d11866233c68fe53f1d64a68183", + "shasum": "" + }, + "require": { + "php": ">=5.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cherry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "ABGEO", + "email": "takalandzet@gmail.com", + "homepage": "http://www.abgeo.ga", + "role": "Developer" + } + ], + "description": "Cherry-project Request class", + "homepage": "https://github.com/ABGEO07/cherry-request", + "keywords": [ + "cherry", + "request" + ], + "time": "2019-03-05 21:14:32" + } + ], "packages-dev": [], "aliases": [], "minimum-stability": "dev", @@ -14,7 +58,8 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=5.6.0" + "php": ">=5.6.0", + "ext-json": "*" }, "platform-dev": [] } diff --git a/src/Router.php b/src/Router.php index 5e1d0d7..ef1da1f 100644 --- a/src/Router.php +++ b/src/Router.php @@ -1,13 +1,164 @@ + * @license https://github.com/ABGEO07/cherry-router/blob/master/LICENSE MIT + * @link https://github.com/ABGEO07/cherry-router + */ namespace Cherry; /** * Cherry project router class * - * @package Cherry - * @author Temuri Takalandze(ABGEO) + * @category Library + * @package Cherry + * @author Temuri Takalandze + * @license https://github.com/ABGEO07/cherry-router/blob/master/LICENSE MIT + * @link https://github.com/ABGEO07/cherry-router */ class Router { + /** + * Path to routes file + * + * @var string + */ + private $_routesFile; + + /** + * Path to controllers folder + * + * @var string + */ + private $_controllersPath; + + /** + * Application routes + * + * @var array + */ + private $_routes; + + /** + * Router constructor. + * + * @param string $routesFile Path to routes file + * @param string $controllersPath Path to controllers folder + */ + public function __construct($routesFile, $controllersPath) + { + $this->_routesFile = $routesFile; + $this->_controllersPath = $controllersPath; + $this->_routes = $this->_getRoutes(); + + $this->_checkRoute(); + } + + /** + * Get routes from routes file + * + * @return array Application routes + */ + private function _getRoutes() + { + $routes = @file_get_contents($this->_routesFile) + or die("Unable to open routes file!"); + + return json_decode($routes, 1); + } + + /** + * Check if current request url math with + * + * @return void + */ + private function _checkRoute() + { + //Get request Parameters + $request = new Request(); + $requestUrl = $request->getPath(); + $requestMethod = $request->getMethod(); + + $routes = $this->_routes; + $controllersPath = $this->_controllersPath; + $routeFound = false; + + foreach ($routes as $route) { + $path = $this->_convertToRE($route['path']); + $method = strtoupper($route['method']); + + $match = array(); + + if ($requestMethod == $method + && preg_match($path, $requestUrl, $match) + ) { + $routeFound = true; + unset($match[0]); + + $action = explode('::', $route['action']); + $controller = explode('\\', $action[0]); + $controllerFile = $controllersPath . '/' . $controller[1] . '.php'; + + //Include controller file + if (file_exists($controllerFile)) { + include_once "$controllerFile"; + } else { + die('Controller ' . $action[0] . ' not found!'); + } + + //Get controllers new object + $object = new $action[0](); + $objMethod = (string)$action[1]; + + if (!method_exists($object, $objMethod)) { + die( + 'Method ' . $objMethod . + ' not found in controller ' . $action[0] + ); + } + + if (empty($match)) { + $object->$objMethod(); + } else { + $object->$objMethod($match); + } + } + } + + if (!$routeFound) { + die("Route {$requestUrl} Not Found!"); + } + } + + /** + * Convert route to regular expression + * + * @param string $plainText Router template for converting + * + * @return string converted to Regular Expression route + */ + private function _convertToRE($plainText) + { + $plainText = str_replace('/', "\/", $plainText); + $lastMatch = 0; + + while ($start = strpos($plainText, '{', $lastMatch)) { + $end = strpos($plainText, '}', $lastMatch); + + //Cut tet for replacing + $changeMe = substr($plainText, $start, $end - $start + 1); + $reName = substr($changeMe, 1, strlen($changeMe) - 2); + $replace = "(?<{$reName}>[a-zA-Z0-9\_\-]+)"; + $plainText = str_replace($changeMe, $replace, $plainText); + $lastMatch = $start + 1; + } + + return "@^{$plainText}$@D"; + } } \ No newline at end of file From f4f1fd5492356b62b122f4da2b807276e85d9824 Mon Sep 17 00:00:00 2001 From: ABGEO Date: Thu, 7 Mar 2019 17:44:02 +0400 Subject: [PATCH 3/6] Add routes caching feature --- .gitignore | 1 + examples/config/config.json | 4 ++ examples/{src => }/config/routes.json | 0 .../controllers/DefaultController.php | 0 examples/test.php | 16 +++-- src/Router.php | 59 ++++++++++++++++--- 6 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 examples/config/config.json rename examples/{src => }/config/routes.json (100%) rename examples/{src => }/controllers/DefaultController.php (100%) diff --git a/.gitignore b/.gitignore index ecdf2d7..50ebd8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ vendor .idea +var* diff --git a/examples/config/config.json b/examples/config/config.json new file mode 100644 index 0000000..f79e695 --- /dev/null +++ b/examples/config/config.json @@ -0,0 +1,4 @@ +{ + "ROUTES_FILE": "config/routes.json", + "CONTROLLERS_PATH": "controllers" +} \ No newline at end of file diff --git a/examples/src/config/routes.json b/examples/config/routes.json similarity index 100% rename from examples/src/config/routes.json rename to examples/config/routes.json diff --git a/examples/src/controllers/DefaultController.php b/examples/controllers/DefaultController.php similarity index 100% rename from examples/src/controllers/DefaultController.php rename to examples/controllers/DefaultController.php diff --git a/examples/test.php b/examples/test.php index 7118f7c..8690475 100644 --- a/examples/test.php +++ b/examples/test.php @@ -3,9 +3,17 @@ //Include autoloader require_once __DIR__ . '/../vendor/autoload.php'; -use Cherry\Router; +define('__ROOT__', __DIR__); +define('CONFIG_FILE', __DIR__ . '/config/config.json'); + +$config = file_get_contents(CONFIG_FILE) + or die("Unable to open routes file!"); + +$config = json_decode($config, 1); -define('ROUTES_FILE', __DIR__ . '/src/config/routes.json'); -define('CONTROLLERS_PATH', __DIR__ . '/src/controllers'); +foreach ($config as $k => $v) + define($k, __DIR__ . '/' . $v); + +use Cherry\Router; -$router = new Router(ROUTES_FILE, CONTROLLERS_PATH); \ No newline at end of file +$router = new Router(); \ No newline at end of file diff --git a/src/Router.php b/src/Router.php index ef1da1f..8ca52f4 100644 --- a/src/Router.php +++ b/src/Router.php @@ -47,14 +47,11 @@ class Router /** * Router constructor. - * - * @param string $routesFile Path to routes file - * @param string $controllersPath Path to controllers folder */ - public function __construct($routesFile, $controllersPath) + public function __construct() { - $this->_routesFile = $routesFile; - $this->_controllersPath = $controllersPath; + $this->_routesFile = ROUTES_FILE; + $this->_controllersPath = CONTROLLERS_PATH; $this->_routes = $this->_getRoutes(); $this->_checkRoute(); @@ -67,9 +64,27 @@ public function __construct($routesFile, $controllersPath) */ private function _getRoutes() { - $routes = @file_get_contents($this->_routesFile) + $originalRoutes = file_get_contents($this->_routesFile) or die("Unable to open routes file!"); + $originalRoutes = json_decode($originalRoutes, 1); + + $routesCacheFile = __ROOT__ . '/var/cache/router.json'; + + if (file_exists($routesCacheFile)) { + $routesCache = json_decode(@file_get_contents($routesCacheFile), 1); + $routesHash = md5(json_encode($originalRoutes)); + $cacheHash = $routesCache['hash']; + + if ($cacheHash !== $routesHash) { + $this->_cacheRoutes($routesCacheFile, $originalRoutes); + } + } else { + $this->_cacheRoutes($routesCacheFile, $originalRoutes); + } + + $routes = file_get_contents($routesCacheFile); + return json_decode($routes, 1); } @@ -89,8 +104,12 @@ private function _checkRoute() $controllersPath = $this->_controllersPath; $routeFound = false; - foreach ($routes as $route) { - $path = $this->_convertToRE($route['path']); + foreach ($routes as $k => $route) { + if ($k == 'hash') { + continue; + } + + $path = $route['path']; $method = strtoupper($route['method']); $match = array(); @@ -136,6 +155,28 @@ private function _checkRoute() } } + /** + * Cache the routes + * + * @param string $routesCacheFile Cache file location. + * @param array $routes Application routes + * + * @return void + */ + private function _cacheRoutes($routesCacheFile, $routes) + { + $hash = md5(json_encode($routes)); + + foreach ($routes as $k => $v) { + $routes[$k]['path'] = $this->_convertToRE($v['path']); + } + + $routes['hash'] = $hash; + + @mkdir(dirname($routesCacheFile), 0755, true); + @file_put_contents($routesCacheFile, json_encode($routes)); + } + /** * Convert route to regular expression * From 05adb868a170503b29cf19c979e5f3bb35296cac Mon Sep 17 00:00:00 2001 From: ABGEO Date: Thu, 7 Mar 2019 17:46:23 +0400 Subject: [PATCH 4/6] Add routes caching feature --- examples/test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/test.php b/examples/test.php index 8690475..1c3f04a 100644 --- a/examples/test.php +++ b/examples/test.php @@ -7,7 +7,7 @@ define('CONFIG_FILE', __DIR__ . '/config/config.json'); $config = file_get_contents(CONFIG_FILE) - or die("Unable to open routes file!"); + or die("Unable to open config file!"); $config = json_decode($config, 1); From cb18f43398d8b36646c025a586d8960974b9b5c6 Mon Sep 17 00:00:00 2001 From: ABGEO Date: Thu, 7 Mar 2019 18:48:45 +0400 Subject: [PATCH 5/6] Update README --- README.md | 105 ++++++++++++++++++++++++++++++++++++++++++++++ examples/test.php | 1 + 2 files changed, 106 insertions(+) diff --git a/README.md b/README.md index 93a6a09..2d50f6c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,109 @@ # Cherry-Router The Cherry-project Router +[![GitHub license](https://img.shields.io/github/license/abgeo07/cherry-router.svg)](https://github.com/ABGEO07/cherry-router/blob/master/LICENSE) + +[![GitHub release](https://img.shields.io/github/release/abgeo07/cherry-router.svg)](https://github.com/ABGEO07/cherry-router/releases) + +[![Packagist Version](https://img.shields.io/packagist/v/cherry-project/router.svg "Packagist Version")](https://packagist.org/packages/cherry-project/router "Packagist Version") + +------------ + +## Including +**Install from composer** `composer require cherry-project/router` + +**Include Autoloader in your main file** (Ex.: index.php) +```php +require_once __DIR__ . '/vendor/autoload.php'; +``` + +Define application root directory +```php +define('__ROOT__', __DIR__); +``` + +In your application you must have **config.json** file for storing app configuration settings and you must define his location: +```php +define('CONFIG_FILE', __DIR__ . '/config/config.json'); +``` + +**config.json** must contain path to **routes.json** and controllers directory + +```json +{ + "ROUTES_FILE": "config/routes.json", + "CONTROLLERS_PATH": "controllers" +} +``` + +Get app config parameters and define it: + +```php +$config = file_get_contents(CONFIG_FILE) + or die("Unable to open config file!"); + +$config = json_decode($config, 1); + +foreach ($config as $k => $v) + define($k, __DIR__ . '/' . $v); +``` + +**Notice**: This approach will be replaced in the new version :)) + +It's time to configure routes file + +The routes file is a json file, where object key is route unique name. + +Each route must have **path**, **method** and **action** keys. Homepage route example: +```json +{ + "homepage": { + "path": "/", + "method": "GET", + "action": "web2hw\\DefaultController::index" + } +} +``` + +**Router file basic structure** +```json +{ + "[RouteName]": { + "path": "[URL]", + "method": "[HTTP_Method]", + "action": "[Namespace]\\[Controller]::[Method]" + } +} +``` + +Definitions for router keys: +- **[RouteName]** - Route unique name; +- **path** - Route url. (Ex.: For address http://www.example.com/homepage [URL] is *homepage*); +- **method** - Route HTTP Method. Allowed all [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods "HTTP methods"); +- **action** - Route callback action. The firs part of action (before *::*) is your controller (stored in **CONTROLLERS_PATH**). +Controller is a simple [PHP Class](http://php.net/manual/en/language.oop5.php "PHP Class") where [Namespace] is his Namespace and +[Controller] Class name (Class name and class filename must have same names (Ex.: **[Controller].php**)). +The second part of action key (after ::) is controllers (class) public method; + +Your route path can use **Placeholders**. Placeholder is a template of your route. + +Route example with placeholder: +```json +{ + "homepage": { + "path": "/hello/{name}", + "method": "GET", + "action": "Cherry\\DefaultController::sayHello" + } +} +``` + +There we have placeholder called **{name}** and we can get this value in controller: +```php +public function sayHello($args) +{ + echo "Hello, {$args['name']}"; +} +``` + **2019 © Cherry-project** diff --git a/examples/test.php b/examples/test.php index 1c3f04a..3fb5b59 100644 --- a/examples/test.php +++ b/examples/test.php @@ -6,6 +6,7 @@ define('__ROOT__', __DIR__); define('CONFIG_FILE', __DIR__ . '/config/config.json'); +//TODO: Create class for defining application config parameters $config = file_get_contents(CONFIG_FILE) or die("Unable to open config file!"); From f844f4d704b38e7ab431a1b527fc6a179177aaec Mon Sep 17 00:00:00 2001 From: ABGEO Date: Thu, 7 Mar 2019 18:53:29 +0400 Subject: [PATCH 6/6] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3738f6..f7f19d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,9 @@ # Cherry-router + +## [v1.0.0](https://github.com/ABGEO07/cherry-router/releases/tag/v1.0.0 "v1.0.0") (2019-03-07) +#### The first stable version + +- Package available on: `composer require cherry-project/router`; +- Router path has placeholder support; +- Router has all HTTP Methods support; +- Router has caching feature; \ No newline at end of file