diff --git a/controller/indexController.php b/controller/exampleController.php similarity index 92% rename from controller/indexController.php rename to controller/exampleController.php index 3e4566c..6dceb06 100644 --- a/controller/indexController.php +++ b/controller/exampleController.php @@ -16,7 +16,7 @@ use Wepesi\Core\Http\Redirect; use Wepesi\Core\Session; -class indexController extends Controller +class exampleController extends Controller { public function __construct() { diff --git a/index.php b/index.php index 581ee30..7413128 100644 --- a/index.php +++ b/index.php @@ -17,7 +17,7 @@ (new DotEnv($ROOT_DIR . '/.env'))->load(); /** - * Generate and index file for redirection (protection) while APP_DEV in production + * Generate and index a file for redirection (protection) while APP_DEV in production */ if (getenv('APP_ENV') === 'prod') { autoIndexFolder(['assets']); @@ -31,6 +31,4 @@ $app = new Application($ROOT_DIR, $configuration); -require_once $app::$ROOT_DIR . '/router/route.php'; - $app->run(); diff --git a/router/api.php b/router/api.php deleted file mode 100644 index 9be1e80..0000000 --- a/router/api.php +++ /dev/null @@ -1,6 +0,0 @@ -api('/', function () use ($router) { - $router->get('/home', function () { - \Wepesi\Core\Http\Response::send(['message' => 'Welcom to api routing']); - }); -}); diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..1406985 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,10 @@ +router(); +$router->get('/', function () { + Response::send('Welcome to Wepesi API'); +}); \ No newline at end of file diff --git a/router/route.php b/routes/web.php similarity index 52% rename from router/route.php rename to routes/web.php index 396b5c6..6197dc0 100644 --- a/router/route.php +++ b/routes/web.php @@ -1,10 +1,16 @@ router(); + // setup get started pages index $router->get('/', function () { (new View)->display('/home'); @@ -13,9 +19,12 @@ $router->get('/helloworld', function () { (new View)->renderHTML('

Hello World!

'); }); -$router->get('/home', [\Wepesi\Controller\indexController::class,'home']); + +$router->get('/home', [exampleController::class,'home']); // -$router->post('/changelang', [indexController::class, 'changeLang']) +$router->post('/changelang', [exampleController::class, 'changeLang']) ->middleware([exampleValidation::class, 'changeLang']); -include \Wepesi\Core\Application::$ROOT_DIR . './router/api.php'; \ No newline at end of file +$router->set404(function(){ + Response::send('route not defined', 404); +}); diff --git a/src/Core/Application.php b/src/Core/Application.php index 51b5a25..4dedffd 100644 --- a/src/Core/Application.php +++ b/src/Core/Application.php @@ -12,39 +12,40 @@ */ class Application { + /** + * @var array + */ + private static array $config_params = []; /** * define application global configuration * @var string */ - public static string $ROOT_DIR; + private static string $root_dir; /** * @var string */ public static string $APP_DOMAIN; /** - * @var string|mixed + * @var string */ public static string $APP_LANG; - /** - * @var string|mixed|null - */ - public static ?string $APP_TEMPLATE; /** * @var string */ - private static string $LAYOUT_CONTENT = 'layout_content'; + public static string $APP_TEMPLATE; /** * @var string */ - private static string $LAYOUT = ''; + public static string $layout_content_param; /** * @var string */ - private static string $VIEW_FOLDER = ''; + private static string $layout; + /** - * @var array + * @var string */ - private static array $params = []; + private static string $VIEW_FOLDER; /** * @var Router */ @@ -52,59 +53,36 @@ class Application /** * Application constructor. - * @param string $path path root directory of the application + * @param string $path root path directory of the application * @param AppConfiguration $config */ public function __construct(string $path, AppConfiguration $config) { - - self::$ROOT_DIR = str_replace("\\", '/', $path); - self::$APP_DOMAIN = $this->domainSetup()->app_domain; - self::$params = $config->generate(); - self::$APP_TEMPLATE = self::$params['app_template'] ?? null; - self::$APP_LANG = self::$params['lang'] ?? 'fr'; + self::$config_params = $config->generate(); + self::$root_dir = str_replace("\\", '/', $path); + self::$APP_DOMAIN = serverDomain()->domain; + self::$APP_LANG = self::$config_params['lang'] ?? 'fr'; + self::$APP_TEMPLATE = self::$config_params['app_template'] ?? ''; + self::$layout_content_param = 'layout_content'; + self::$layout = ''; + self::$VIEW_FOLDER = ''; $this->router = new Router(); - self::$LAYOUT_CONTENT = 'layout_content'; - } - - /** - * @return object - */ - private function domainSetup(): object - { - $server_name = $_SERVER['SERVER_NAME']; - $protocol = strtolower(explode('/', $_SERVER['SERVER_PROTOCOL'])[0]); - $domain = self::getDomainIp() === '127.0.0.1' ? "$protocol://$server_name" : $server_name; - return (object)[ - 'server_name' => $server_name, - 'protocol' => $protocol, - 'app_domain' => $domain, - ]; } /** - * use method to get domain ip * @return string */ - public static function getDomainIp() : string + public static function getRootDir(): string { - $ip = $_SERVER['REMOTE_ADDR']; - - if (! empty($_SERVER['HTTP_CLIENT_IP'])) { - $ip = $_SERVER['HTTP_CLIENT_IP']; - } elseif (! empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { - $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; - } elseif ($ip == '::1') { - $ip = gethostbyname(getHostName()); - } - return $ip; + return self::$root_dir; } + /** * simple builtin dumper for dump data * @param $ex - * @return void + * */ - public static function dumper($ex) + public static function dumper($ex): void { print('
');
         var_dump($ex);
@@ -113,32 +91,53 @@ public static function dumper($ex)
     }
 
     /**
-     * Set the layout at the top of your application to be available everywhere.
+     * Define a layout to be used by all pages in the application.
+     * can be set at the top of your application to be available everywhere.
      * @param string $layout
      * @return void
      */
     public static function setLayout(string $layout)
     {
-        self::$LAYOUT = self::$ROOT_DIR.'/views/'.$layout;
+        self::$layout = self::getRootDir() . '/views/' . trim($layout, '/');
     }
-    public static function setLayoutContent(string $layout_name)
+
+    /**
+     * @param string $layout_name
+     * @return void
+     */
+    public static function setLayoutcontentparam(string $layout_name)
     {
-        self::$LAYOUT_CONTENT = $layout_name;
+        self::$layout_content_param = $layout_name;
     }
 
+    /**
+     * @param string $folder_name
+     * @return void
+     */
     public static function setViewFolder(string $folder_name)
     {
         self::$VIEW_FOLDER = $folder_name;
     }
-    public static function getLayout()
+
+    /**
+     * @return string
+     */
+    public static function getLayout(): string
     {
-        return self::$LAYOUT ;
+        return trim(self::$layout );
     }
-    public static function getLayoutContent()
+
+    /**
+     * @return string
+     */
+    public static function getLayoutContentParam(): string
     {
-        return self::$LAYOUT_CONTENT ;
+        return self::$layout_content_param ;
     }
 
+    /**
+     * @return string
+     */
     public static function getViewFolder()
     {
         return self::$VIEW_FOLDER ;
@@ -154,9 +153,69 @@ public function router(): Router
 
     /**
      * @return void
+     * @throws \Exception
+     */
+    protected function routeProvider(): void
+    {
+        $base_route_path = self::getRootDir() . '/routes';
+        $api_route_path = $base_route_path . '/api.php';
+        if (file_exists($api_route_path)) {
+            $this->router->group([
+                'pattern' => '/api'
+            ], function (Router $router) {
+                if (isset($_SERVER['HTTP_ORIGIN'])) {
+                    // Decide if the origin in $_SERVER['HTTP_ORIGIN'] is one
+                    // you want to allow, and if so:
+                    header('Access-Control-Allow-Origin: *');
+                    header('Access-Control-Allow-Credentials: true');
+                    header('Access-Control-Max-Age: 86400');    // cache for 1 day
+                }
+                header('Access-Control-Allow-Methods: GET, POST,PUT, PATCH, HEAD, OPTIONS');
+                // Access-Control headers are received during OPTIONS requests
+                if ($_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
+                    // may also be using PUT, PATCH, HEAD etc.
+                    if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']))
+                        header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}");
+
+                    exit(0);
+                }
+                $router->group([], $this->registerRoute('/api.php'));
+            });
+        }
+        $web_route_path = $base_route_path . '/web.php';
+        if (file_exists($web_route_path)) {
+            $this->router->group([], $this->registerRoute('/web.php'));
+        }
+        if (!file_exists($web_route_path) && !file_exists($api_route_path)) {
+            throw new \Exception('No Route file not found.');
+        }
+    }
+
+    /**
+     * route path
+     * @param string $path
+     * @return string
+     */
+    public function registerRoute(string $path): string
+    {
+        return $this->basePath('/routes' . '/' . trim($path,'/'));
+    }
+    /**
+     * @param string $path
+     * @return string
+     */
+    public function basePath(string $path): string
+    {
+        return self::$root_dir . '/' . trim($path,'/');
+    }
+
+    /**
+     * @return void
+     * @throws \Exception
      */
-    public function run()
+    public function run(): void
     {
+        $this->routeProvider();
         $this->router->run();
     }
 }
\ No newline at end of file
diff --git a/src/Core/Bundles.php b/src/Core/Bundles.php
index ef920e9..f8f1673 100644
--- a/src/Core/Bundles.php
+++ b/src/Core/Bundles.php
@@ -10,12 +10,12 @@ class Bundles
     private static array $header_link = [];
 
     /**
-     * manage to add a javascript script on the page
+     * manage to add a JavaScript script on the page
      * @param string $file
      */
     static function insertCSS(string $file)
     {
-        if (is_file(Application::$ROOT_DIR . '/assets/css/' . $file . '.css')) {
+        if (is_file(Application::getRootDir() . '/assets/css/' . $file . '.css')) {
             $href = WEB_ROOT . "assets/css/$file.css";
             $link = <<
diff --git a/src/Core/Routing/Providers/Contracts/RouteContract.php b/src/Core/Routing/Providers/Contracts/RouteContract.php
new file mode 100644
index 0000000..6438ad8
--- /dev/null
+++ b/src/Core/Routing/Providers/Contracts/RouteContract.php
@@ -0,0 +1,14 @@
+
  */
-class Route
+class Route implements RouteContract
 {
     /**
      * @var string
@@ -42,10 +44,11 @@ class Route
     use routeBuilder;
 
     /**
-     * @param $path
+     * @param class-string $path
      * @param $callable
+     * @param array $middleware
      */
-    function __construct($path, $callable, array $middleware = [])
+    function __construct(string $path, $callable, array $middleware = [])
     {
         $this->pattern = trim($path, '/');
         $this->callable = $callable;
@@ -56,10 +59,10 @@ function __construct($path, $callable, array $middleware = [])
     }
 
     /**
-     * @param $url
+     * @param class-string $url
      * @return bool
      */
-    function match($url): bool
+    function match(string $url): bool
     {
         $url = trim($url, '/');
         $path = preg_replace_callback('#:([\w]+)#', [$this, 'paramMatch'], $this->pattern);
@@ -80,16 +83,16 @@ function match($url): bool
     /**
      *
      */
-    public function call()
+    public function call(): void
     {
         try {
             if (count($this->middleware_tab) > 0) {
                 foreach ($this->middleware_tab as $middleware) {
-                    $this->routeFunctionCall($middleware, true, $this->_matches);
+                    $this->executeMiddleware($middleware, $this->_matches);
                 }
                 $this->middleware_tab = [];
             }
-            $this->routeFunctionCall($this->callable, false, $this->_matches);
+            $this->executeController($this->callable, $this->_matches);
         } catch (\Exception $ex) {
             echo $ex->getMessage();
         }
@@ -126,7 +129,7 @@ public function getPattern(): string
      * @param $params
      * @return array|string|string[]
      */
-    public function getUrl($params)
+    public function getUrl($params): array|string
     {
         $path = $this->pattern;
         foreach ($params as $k => $v) {
diff --git a/src/Core/Routing/RouteFileRegistrar.php b/src/Core/Routing/RouteFileRegistrar.php
new file mode 100644
index 0000000..ab97e15
--- /dev/null
+++ b/src/Core/Routing/RouteFileRegistrar.php
@@ -0,0 +1,46 @@
+
+ */
+final class RouteFileRegistrar implements RouterProviders
+{
+    /**
+     * @var RouterContract
+     */
+    protected RouterContract $router;
+
+    /**
+     * Create a new route file registrar instance.
+     *
+     * @param Router $router
+     */
+    public function __construct(RouterContract $router)
+    {
+        $this->router = $router;
+    }
+
+    /**
+     * Require the given routes file.
+     *
+     * @param class-string $routes
+     * @return void
+     */
+    public function register(string $routes): void
+    {
+        $router = $this->router;
+        if (is_file($routes)) {
+            require $routes;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Core/Routing/Router.php b/src/Core/Routing/Router.php
index 1b348e6..c90d51c 100644
--- a/src/Core/Routing/Router.php
+++ b/src/Core/Routing/Router.php
@@ -5,18 +5,23 @@
 
 namespace Wepesi\Core\Routing;
 
+use Closure;
 use Wepesi\Core\Application;
+use Wepesi\Core\Escape;
 use Wepesi\Core\Exceptions\RoutingException;
 use Wepesi\Core\Http\Response;
+use Wepesi\Core\Routing\Providers\Contracts\RouteContract;
+use Wepesi\Core\Routing\Providers\Contracts\RouterContract;
 use Wepesi\Core\Routing\Traits\routeBuilder;
 
 /**
- *  Wepesi API Router provider
+ * @template T
+ * @template-implements RouterContract
  */
-class  Router
+class  Router implements RouterContract
 {
     /**
-     * @var array|null
+     * @var array
      */
     protected array $baseMiddleware;
     /**
@@ -56,7 +61,7 @@ public function __construct()
     }
 
     /**
-     * @param $path
+     * @param string $path
      * @param $callable
      * @param null $name
      * @return Route
@@ -73,9 +78,9 @@ public function get(string $path, $callable, $name = null): Route
      * @param string $methode
      * @return Route
      */
-    private function add(string $pattern, $callable, ?string $name, string $methode): Route
+    private function add(string $pattern, $callable, ?string $name, string $methode): RouteContract
     {
-        $pattern = $this->baseRoute . '/' . trim($pattern, '/');
+        $pattern = $this->baseRoute . Escape::addSlashes(trim($pattern, '/'));
         $pattern = $this->baseRoute ? rtrim($pattern, '/') : $pattern;
         $route = new Route($pattern, $callable, $this->baseMiddleware);
         $this->routes[$methode][] = $route;
@@ -91,7 +96,7 @@ private function add(string $pattern, $callable, ?string $name, string $methode)
     }
 
     /**
-     * @param $path
+     * @param string $path
      * @param $callable
      * @param null $name
      * @return Route
@@ -117,24 +122,6 @@ public function delete(string $path, $callable, $name = null): Route
         return $this->add($path, $callable, $name, 'DELETE');
     }
 
-    /**
-     * API base group routing
-     * @param string|array $base_route it can be defined as we did for group routing, but you don't need to specify api, it will be added automatically
-     * @param callable $callable
-     * @return null
-     */
-    public function api($base_route, callable $callable)
-    {
-        $api_pattern = '/api';
-        if (is_array($base_route)) {
-            $base_route['pattern'] = $api_pattern . (isset($base_route['pattern']) ? $this->trimPath($base_route['pattern']) : '');
-        } else {
-
-            $base_route = $api_pattern . $this->trimPath($base_route);
-        }
-        return $this->group($base_route, $callable);
-    }
-
     /**
      * @param string $path
      * @return string
@@ -142,7 +129,7 @@ public function api($base_route, callable $callable)
     private function trimPath(string $path): string
     {
         $trim_path = trim($path, '/');
-        return strlen($trim_path) > 0 ? '/' . $trim_path : '';
+        return strlen($trim_path) > 0 ? Escape::addSlashes($trim_path) : '';
     }
 
     /**
@@ -151,7 +138,7 @@ private function trimPath(string $path): string
      * @param array|string $base_route can be a string or an array to defined middleware for the group routing
      * @param callable $callable a callable method can be a controller method or an anonymous callable method
      */
-    public function group($base_route, callable $callable)
+    public function group(array|string $base_route, $callable): void
     {
         $pattern = $base_route;
         if (is_array($base_route)) {
@@ -162,7 +149,12 @@ public function group($base_route, callable $callable)
         }
         $cur_base_route = $this->baseRoute;
         $this->baseRoute .= $pattern;
-        call_user_func($callable);
+
+        if ($callable instanceof Closure) {
+            call_user_func($callable, $this);
+        } else {
+            (new RouteFileRegistrar($this))->register($callable);
+        }
         $this->baseRoute = $cur_base_route;
     }
 
@@ -206,12 +198,12 @@ protected function getUrl()
     }
 
     /**
-     * Set the 404 handling function.
+     * Set the 404 handling functions.
      *
-     * @param object|callable|string $match_fn The function to be executed
+     * @param callable|object|string $match_fn The function to be executed
      * @param $callable
      */
-    public function set404($match_fn, $callable = null)
+    public function set404(callable|object|string $match_fn, $callable = null): void
     {
         if (!$callable) {
             $this->notFoundCallback = $match_fn;
@@ -221,7 +213,15 @@ public function set404($match_fn, $callable = null)
     }
 
     /**
-     * @return void
+     * list all routes elements
+     * @return array
+     */
+    public function getRoutes(): array
+    {
+        return $this->routes;
+    }
+    /**
+     * Execute the route request
      */
     public function run()
     {
@@ -240,7 +240,6 @@ public function run()
             }
             if (count($routesRequestMethod) === $i) {
                 $this->trigger404($this->notFoundCallback);
-                Response::setStatusCode(404);
             }
         } catch (RoutingException $ex) {
             Application::dumper($ex);
@@ -250,9 +249,9 @@ public function run()
     }
 
     /**
-     * @return void
+     * trigger 404 error while no route match
      */
-    protected function trigger404($match = null)
+    protected function trigger404($match = null): void
     {
         if ($match) {
             $this->routeFunctionCall($match);
diff --git a/src/Core/Routing/Traits/routeBuilder.php b/src/Core/Routing/Traits/routeBuilder.php
index cfd7315..f2db849 100644
--- a/src/Core/Routing/Traits/routeBuilder.php
+++ b/src/Core/Routing/Traits/routeBuilder.php
@@ -7,39 +7,45 @@
 
 use Wepesi\Core\Application;
 
+/**
+ *
+ */
 trait routeBuilder
 {
-    protected function routeFunctionCall($callable, bool $is_middleware = false, array $matches = []): void
+    /**
+     * @param $callable
+     * @param array $matches
+     * @param bool $is_middleware
+     * @return void
+     */
+    protected function routeFunctionCall($callable, array $matches = [], bool $is_middleware = false): void
     {
-        $controller = !$is_middleware ? 'controller' : 'middleware';
+        $class_element = !$is_middleware ? 'controller' : 'middleware';
         try {
             if (is_string($callable) || is_array($callable)) {
-                $params = is_string($callable) ? explode('#', $callable) : $callable;
-                if (count($params) != 2) {
-                    throw new \InvalidArgumentException("Error : on `$controller` class/method is not well defined");
+                $callable_params = is_string($callable) ? explode('#', $callable) : $callable;
+                if (count($callable_params) != 2) {
+                    throw new \InvalidArgumentException("Error : on `$class_element` class/method is not well defined");
                 }
 
-                $classCallable = $params[0];
-                $class_method = $params[1];
-                if (!class_exists($classCallable, true)) {
-                    throw new \InvalidArgumentException("$classCallable class not defined, not a valid $controller", 500);
+                $class_name = $callable_params[0];
+                $class_method_name = $callable_params[1];
+                if (!class_exists($class_name, true)) {
+                    throw new \InvalidArgumentException("$class_name class not defined, not a valid $class_element", 500);
                 }
 
-                $reflection = new \ReflectionClass($classCallable);
+                $reflection = new \ReflectionClass($class_name);
                 $class_instance = $reflection->newInstance();
 
                 if (!$reflection->isInstance($class_instance)) {
                     throw new \ReflectionException('Only instantiable class can be used. Not abstract,interface, or trait can be used.');
                 }
-                if (!method_exists($class_instance, $class_method)) {
-                    throw new \BadMethodCallException("method : $class_method does not belong the class : $classCallable.", 500);
-                }
-                call_user_func_array([$class_instance, $class_method], $matches);
-            } else {
-                $closure = $callable;
-                if (isset($closure) && is_callable($closure, true)) {
-                    call_user_func_array($closure, $matches);
+                if (!method_exists($class_instance, $class_method_name)) {
+                    throw new \BadMethodCallException("method : $class_method_name does not belong the class : $class_name.", 500);
                 }
+                call_user_func_array([$class_instance, $class_method_name], $matches);
+            } else if (isset($callable) && is_callable($callable, true)) {
+                call_user_func_array($callable, $matches);
             }
             return;
         } catch (\Exception $ex) {
@@ -47,4 +53,14 @@ protected function routeFunctionCall($callable, bool $is_middleware = false, arr
         }
     }
 
+    protected function executeController($controller, array $matches = []): void
+    {
+        $this->routeFunctionCall($controller, $matches);
+    }
+
+    protected function executeMiddleware($controller, array $matches = []): void
+    {
+        $this->routeFunctionCall($controller, $matches, true);
+    }
+
 }
\ No newline at end of file
diff --git a/src/Core/Views/Provider/ViewBuilderProvider.php b/src/Core/Views/Provider/ViewBuilderProvider.php
index 03923c8..620ed9e 100644
--- a/src/Core/Views/Provider/ViewBuilderProvider.php
+++ b/src/Core/Views/Provider/ViewBuilderProvider.php
@@ -87,7 +87,7 @@ public function renderHTML(string $html): void
      */
     public function setLayout(string $template): void
     {
-        $this->layout = Application::$ROOT_DIR . '/views' . $template;
+        $this->layout = Application::getRootDir() . '/views' . $template;
     }
 
     /**
diff --git a/src/Core/Views/View.php b/src/Core/Views/View.php
index 92a7763..4819273 100644
--- a/src/Core/Views/View.php
+++ b/src/Core/Views/View.php
@@ -77,15 +77,22 @@ public function display(string $view): void
     {
         $view_file = $this->buildFilePath($view);
         $render = $this->renderView($view_file);
-        if ($this->layout === '' && !$this->reset) {
+        if (! $this->getLayout() && !$this->reset) {
             $this->layout = Application::getLayout();
         }
-        if ($this->layout !== '') {
+        if ($this->getLayout()) {
             $render = $this->renderLayout($render);
         }
         $this->buildAssetHead($render);
     }
 
+    /**
+     * @return string|null
+     */
+    public function getLayout(): ?string
+    {
+        return strlen(trim($this->layout)) > 0 ? $this->layout : null;
+    }
     /**
      * @param class-string $file_name
      *
@@ -96,7 +103,7 @@ private function buildFilePath(string $file_name): ?string
         $folder = strlen(trim($this->folder_name)) > 0 ? $this->folder_name : Application::getViewFolder();;
         $view_file = Escape::checkFileExtension($file_name);
         $file_source = $folder . Escape::addSlashes($view_file);
-        return Application::$ROOT_DIR . '/views' . $file_source;
+        return Application::getRootDir() . '/views' . $file_source;
     }
 
     /**
@@ -137,7 +144,7 @@ private function renderNotDefined(string $file_name): void
     protected function renderLayout(string $view)
     {
         if ($this->layout_content === '') {
-            $this->layout_content = Application::getLayoutContent();
+            $this->layout_content = Application::getLayoutContentParam();
         }
         if ($this->layout && is_file($this->layout)) {
             $layout_data = $this->data;
@@ -221,5 +228,4 @@ private function generateJSLink(string $path): string
     private function generateStyleLink(string $path): string {
         return '';
     }
-
 }