├── .github └── workflows │ └── php.yml ├── LICENSE ├── README.md ├── composer.json └── lib ├── RouteOne.php ├── RouteOneCli.php ├── routeonecli └── templates ├── htaccess_template.php └── route_template.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Validate composer.json and composer.lock 21 | run: composer validate --strict 22 | 23 | - name: Cache Composer packages 24 | id: composer-cache 25 | uses: actions/cache@v3 26 | with: 27 | path: vendor 28 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-php- 31 | 32 | - name: Install dependencies 33 | run: composer install --prefer-dist --no-progress 34 | 35 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 36 | # Docs: https://getcomposer.org/doc/articles/scripts.md 37 | 38 | # - name: Run test suite 39 | # run: composer run-script test 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RouteOne 2 | It reads the URL route and parses the values of path, so it could be interpreted manually or automatically in the fastest way possible (for example, to implement an MVC system). 3 | 4 | [![Packagist](https://img.shields.io/packagist/v/eftec/routeone.svg)](https://packagist.org/packages/eftec/routeone) 5 | [![Total Downloads](https://poser.pugx.org/eftec/routeone/downloads)](https://packagist.org/packages/eftec/routeone) 6 | [![Maintenance](https://img.shields.io/maintenance/yes/2025.svg)]() 7 | [![composer](https://img.shields.io/badge/composer-%3E1.6-blue.svg)]() 8 | [![php](https://img.shields.io/badge/php-%3E7.4-green.svg)]() 9 | [![php](https://img.shields.io/badge/php-%3E8.4-green.svg)]() 10 | [![coverage](https://img.shields.io/badge/coverage-80%25-green)]() 11 | [![compatible](https://img.shields.io/badge/compatible-linux%7Cwindows%7Cmacos-green)]() 12 | 13 | Unlikely other libraries, this library does not have dependencies, and it is contained in a single class, so it is compatible with any PHP project, for example WordPress, Laravel, Drupal, a custom PHP project, etc. 14 | 15 | This library is based in **CoC Convention over Configuration**. It reduces the boilerplate but it has fixed functionalities. This library does not allow to use custom "routes" but it covers practically all cases, so it increases the performance and usability while it sacrifices flexibility. 16 | 17 | ## Table of contents 18 | 19 | 20 | * [RouteOne](#routeone) 21 | * [Table of contents](#table-of-contents) 22 | * [Example:](#example) 23 | * [What it does?](#what-it-does) 24 | * [What is **$id**, **$idparent** and **$event**?](#what-is-id-idparent-and-event) 25 | * [**id**](#id) 26 | * [**idparent**](#idparent) 27 | * [**event**](#event) 28 | * [**Module**](#module) 29 | * [Getting started](#getting-started) 30 | * [Using the cli (recommended)](#using-the-cli-recommended) 31 | * [manual installation](#manual-installation) 32 | * [1) Create a .htaccess file in the folder root (Apache)](#1-create-a-htaccess-file-in-the-folder-root-apache) 33 | * [Or configure nginx.conf (Nginx) Linux (not tested)](#or-configure-nginxconf-nginx-linux-not-tested) 34 | * [Or configure nginx.conf (Nginx) Windows](#or-configure-nginxconf-nginx-windows) 35 | * [Using Paths](#using-paths) 36 | * [clearPath()](#clearpath) 37 | * [addPath()](#addpath) 38 | * [fetchPath()](#fetchpath) 39 | * [Methods](#methods) 40 | * [__construct($base='', $forcedType=null, $isModule=false)](#__constructbase-forcedtypenull-ismodulefalse) 41 | * [getQuery($key,$valueIfNotFound=null)](#getquerykeyvalueifnotfoundnull) 42 | * [setQuery($key,$value)](#setquerykeyvalue) 43 | * [fetch](#fetch) 44 | * [callObjectEx](#callobjectex) 45 | * [callFile($fileStructure='%s.php',$throwOnError=true)](#callfilefilestructuresphpthrowonerrortrue) 46 | * [getHeader()](#getheader) 47 | * [getBody()](#getbody) 48 | * [](#) 49 | * [getCurrentUrl($withoutFilename = true)](#getcurrenturlwithoutfilename--true) 50 | * [getCurrentServer()](#getcurrentserver) 51 | * [setCurrentServer($serverName)](#setcurrentserverservername) 52 | * [getUrl($extraQuery = '',$includeQuery=false)](#geturlextraquery--includequeryfalse) 53 | * [url($module,$controller,$action,$id,$idparent)](#urlmodulecontrolleractionididparent) 54 | * [urlFront($module,$category,$subcategory,$subsubcategory,$id)](#urlfrontmodulecategorysubcategorysubsubcategoryid) 55 | * [alwaysWWW($https = false)](#alwayswwwhttps--false) 56 | * [alwaysHTTPS()](#alwayshttps) 57 | * [alwaysNakedDomain($https = false)](#alwaysnakeddomainhttps--false) 58 | * [fields](#fields) 59 | * [Whitelist](#whitelist) 60 | * [Whitelist input.](#whitelist-input) 61 | * [CLI](#cli) 62 | * [Changelog](#changelog) 63 | 64 | 65 | ## Example: 66 | 67 | Let's say we have the next URL http://somedomain.dom/Customer/Update/2 This library converts this URL into variables that 68 | could be process or directly calling a method. 69 | 70 | route.php 71 | ```php 72 | $route=new RouteOne('http://www.somedomain.dom'); 73 | $route->addPath('api/{controller}/{action}/{id}'); 74 | $route->addPath('{controller}/{action}/{id}/{idparent}'); 75 | $route->fetchPath(); 76 | $this->callObjectEx('cocacola\controller\{controller}Controller'); 77 | ``` 78 | 79 | controller\CustomerController.php class 80 | ```php 81 | namespace cocacola\controller\; 82 | class CustomerController { 83 | public function updateAction($id=null,$idparent=null,$event=null) { 84 | echo "We want to update the customer $id"; 85 | } 86 | } 87 | 88 | ``` 89 | 90 | 91 | ## What it does? 92 | 93 | Let's say we do the next operation: 94 | 95 | A user calls the next website http://somedomain.com/Customer/Insert, he wants to show a form to insert a customer 96 | 97 | ```php 98 | use \eftec\routeone\RouteOne; 99 | $route=new RouteOne('.',null,null); // Create the RouteOneClass 100 | $route->fetch(); // fetch all the input values (from the route, get, post and such). 101 | $route->callObject('somenamespace\\controller\\%sController'); // where it will call the class CustomerController* 102 | ``` 103 | 104 | or 105 | 106 | ```php 107 | use eftec\routeone\RouteOne; 108 | $route=new RouteOne('.',null,null); // Create the RouteOneClass 109 | $route->fetch(); // fetch all the input values (from the route, get, post and such). 110 | $route->callObjectEx('somenamespace\\controller\\{controller}Controller'); // where it will call the class CustomerController* 111 | ``` 112 | 113 | 114 | This code calls to the method **InsertActionGet** (GET), **InsertActionPost** (POST) or **InsertAction** (GET/POST) 115 | inside the class **Customer** 116 | 117 | The method called is written as follows: 118 | 119 | ```php 120 | class Customer { 121 | public function insertAction($id="",$idparent="",$event="") { 122 | // here we do our operation. 123 | } 124 | } 125 | ``` 126 | 127 | ### What is **$id**, **$idparent** and **$event**? 128 | 129 | ### **id** 130 | 131 | Let's se we want to **Update** a **Customer** number **20**, then we could call the next page 132 | 133 | > http://somedomain.com/Customer/Update/20 134 | 135 | where 20 is the "$id" of the customer to edit (it could be a number of a string) 136 | 137 | ### **idparent** 138 | 139 | And what if we want to **Update** a **Customer** number **20** of the business **APPL** 140 | 141 | > http://somedomain.com/Customer/Update/20/APPL 142 | 143 | Where APPL is the **idparent** 144 | 145 | ### **event** 146 | 147 | Now, let's say we click on some button, or we do some action. It could be captured by the field **_event**, and it 148 | is read by the argument **$event**. This variable could be sent via GET or POST. 149 | 150 | > http://somedomain.com/Customer/Update/20/APPL?_event=click 151 | 152 | ### **Module** 153 | 154 | > Note: Modules are obtained automatically if you use addPath() and fetchPath(), so you don't need to specify it. 155 | Now, let's say our system is modular, and we have several customers (internal customers, external, etc.) 156 | 157 | ```php 158 | $route=new RouteOne('.',null,true); // true indicates it is modular. 159 | ``` 160 | 161 | or 162 | 163 | ```php 164 | $route=new RouteOne('.',null,['Internal']); // or we determine the module automatically. In this case, every url that starts with Internal 165 | ``` 166 | 167 | then 168 | 169 | ```php 170 | $route->fetch(); 171 | $route->callObject('somenamespace\\%2s%\\controller\\%1sController'); 172 | ``` 173 | 174 | > http://somedomain.com/Internal/Customer/Update/20/APPL?_event=click 175 | 176 | Then, the first ramification is the name of the module (**Internal**) and it calls the class **somenamespace\Internal\controller\CustomerController** 177 | 178 | 179 | ## Getting started 180 | 181 | ### Using the cli (recommended) 182 | 183 | * Install the library 184 | 185 | > composer require eftec/routeone 186 | 187 | * Execute the binary in the root folder 188 | 189 | Linux: 190 | 191 | ```shell 192 | vendor/bin/routeonecli -init (if the binary does not work, then chage the permission to execution) 193 | ``` 194 | Windows: 195 | ```shell 196 | .\vendor\bin\routeonecli.bat -init 197 | ``` 198 | 199 | It will create the file .htaccess and the file route.php and route.php will have a default configuration. 200 | 201 | * Edit the file route.php and change the next lines: 202 | 203 | ```php 204 | const BASEURL="http://localhost"; // Base url edit this value. 205 | const BASEWEBNS="eftec\\controller"; // Base namespace (web) edit this value 206 | const BASEAPINS="eftec\\api"; // Base namespace (api) edit this value 207 | ``` 208 | Later, you can add or edit the code in this file. 209 | 210 | #### manual installation 211 | 212 | #### 1) Create a .htaccess file in the folder root (Apache) 213 | 214 | ``` 215 | 216 | 217 | Options -MultiViews -Indexes 218 | 219 | 220 | RewriteEngine On 221 | DirectoryIndex route.php 222 | 223 | # Handle Authorization Header 224 | RewriteCond %{HTTP:Authorization} . 225 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 226 | 227 | # Redirect Trailing Slashes If Not A Folder... 228 | RewriteCond %{REQUEST_FILENAME} !-d 229 | RewriteCond %{REQUEST_URI} (.+)/$ 230 | RewriteRule ^ %1 [L,R=301] 231 | 232 | # Send Requests To Front Controller... 233 | RewriteCond %{REQUEST_FILENAME} !-d 234 | RewriteCond %{REQUEST_FILENAME} !-f 235 | RewriteRule ^ route.php?req=$1 [L,QSA] 236 | 237 | ``` 238 | 239 | > If your web host doesn't allow the FollowSymlinks option, try replacing it with Options +SymLinksIfOwnerMatch. 240 | 241 | 242 | > The important line is: 243 | > RewriteRule ^(.*)$ route.php?req=$1 [L,QSA] # The router to call. 244 | 245 | #### Or configure nginx.conf (Nginx) Linux (not tested) 246 | 247 | ``` 248 | server { 249 | listen 80; 250 | server_name localhost; 251 | root /example.com/public; 252 | 253 | add_header X-Frame-Options "SAMEORIGIN"; 254 | add_header X-XSS-Protection "1; mode=block"; 255 | add_header X-Content-Type-Options "nosniff"; 256 | 257 | index index.html index.htm index.php; 258 | 259 | charset utf-8; 260 | 261 | location / { 262 | try_files $uri $uri/ /router.php?req=$document_uri&$query_string; 263 | } 264 | 265 | location = /favicon.ico { access_log off; log_not_found off; } 266 | location = /robots.txt { access_log off; log_not_found off; } 267 | 268 | error_page 404 /index.php; 269 | 270 | location ~ \.php$ { 271 | fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; 272 | fastcgi_index index.php; 273 | fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; 274 | include fastcgi_params; 275 | } 276 | 277 | location ~ /\.(?!well-known).* { 278 | deny all; 279 | } 280 | } 281 | 282 | ``` 283 | 284 | > The important line is: 285 | > try_files $uri $uri/ /router.php?req=$document_uri&$query_string; 286 | 287 | #### Or configure nginx.conf (Nginx) Windows 288 | 289 | ``` 290 | server { 291 | listen 80; 292 | server_name localhost; 293 | root c:/www; 294 | 295 | add_header X-Frame-Options "SAMEORIGIN"; 296 | add_header X-XSS-Protection "1; mode=block"; 297 | add_header X-Content-Type-Options "nosniff"; 298 | 299 | index index.html index.htm index.php; 300 | 301 | charset utf-8; 302 | 303 | location / { 304 | try_files $uri $uri/ /router.php?req=$document_uri&$query_string; 305 | } 306 | 307 | location = /favicon.ico { access_log off; log_not_found off; } 308 | location = /robots.txt { access_log off; log_not_found off; } 309 | 310 | error_page 404 /index.php; 311 | 312 | location ~ \.php$ { 313 | fastcgi_pass 127.0.0.1:9000; 314 | fastcgi_index index.php; 315 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 316 | include fastcgi_params; 317 | } 318 | 319 | location ~ /\.(?!well-known).* { 320 | deny all; 321 | } 322 | } 323 | 324 | ``` 325 | > The important line is: 326 | > try_files $uri $uri/ /router.php?req=$document_uri&$query_string; 327 | 328 | 329 | where **router.php** is the file that it will work as router. ?req=$1 is important because the system will read the route from "req" 330 | 331 | ```php 332 | // router.php 333 | $route=new RouteOne(); // Create the RouteOneClass 334 | $route->fetch(); // fetch all the input values (from the route, get, post and such). 335 | $route->callObject('somenamespace\\controller\\%sController'); // where it will call the class \somenamespace\controller\CustomerController 336 | ``` 337 | 338 | > Note: 339 | > 340 | > If you want to use an argument different as "req", then you can change it using the next code: 341 | > 342 | > $route->argumentName='newargument'; 343 | 344 | 345 | 346 | ## Using Paths 347 | 348 | Since 1.21, it is possible to use a custom path instead of a pre-defined path. It is the recommended way. 349 | The other method is still present. 350 | 351 | ### clearPath() 352 | 353 | Syntax: 354 | 355 | >clearPath() 356 | 357 | It clears all the paths defined 358 | 359 | ### addPath() 360 | Syntax: 361 | > addPath($path, $name = null,callable $middleWare=null) 362 | 363 | It adds a paths that could be evaluated using fetchPath() 364 | **Example:** 365 | ```php 366 | $this->addPath('api/{controller}/{action}/{id:0}','apipath'); 367 | $this->addPath('/api/{controller}/{action}/{id:0}/','apipath'); // "/" at the beginner and end are trimmed. 368 | $this->addPath('{controller}/{action}/{id:0}','webpath'); 369 | $this->addPath('{controller:root}/{action}/{id:0}','webpath'); // root path using default 370 | $this->addPath('somepath','namepath', 371 | function(callable $next,$id=null,$idparent=null,$event=null) { 372 | echo "middleware\n"; 373 | $result=$next($id,$idparent,$event); // calling the controller 374 | echo "endmiddleware\n"; 375 | return $result; 376 | }); 377 | ``` 378 | **Note:** 379 | The first part of the path, before the "{" is used to determine which path will be used. 380 | Example "path/{controller}" and "path/{controller}/{id}", the system will consider that are the same path 381 | 382 | * **parameter** string $path The path, example "aaa/{controller}/{action:default}/{id}" 383 | Where **default** is the optional default value. 384 | * **{controller}**: The controller (class) to call 385 | * **{action}**: The action (method) to call 386 | * **{verb}**: The verb of the action (GET/POST,etc.) 387 | * **{type}**: The type (value) 388 | * **{module}**: The module (value) 389 | * **{id}**: The id (value) 390 | * **{idparent}**: The id parent (value) 391 | * **{category}**: The category (value) 392 | * **{subcategory}**: The subcategory (value) 393 | * **{subsubcategory}**: The subsubcategory (value) 394 | 395 | * **parameter** string|null $name (optional), the name of the path 396 | * **parameter** callable|null $middleWare A callable function used for middleware. 397 | The first argument of the function must be a callable method 398 | The next arguments must be the arguments defined by callObjectEx 399 | (id,idparent,event) 400 | 401 | * The path could start with a static location but the rest of the path must be defined by variables (enclosed by {}) 402 | and separated by "/". 403 | * You can also set a default value for a path by writing ":" after the name of the variable: {name:defaultvalue} 404 | * The **name** could be obtained using $this->currentPath. If you add a name with the same name, then it is replaced. 405 | * If you don't set a name, then it uses an autonumeric. 406 | * The **name** is also returned when you call $this->fetchPath() 407 | 408 | 409 | Example: 410 | 411 | ```php 412 | $this->addPath('{controller}/{id}/{idparent}'); 413 | $this->addPath('myapi/otherfolder/{controller}/{id}/{idparent}'); 414 | $this->addPath('{controller:defcontroller}/{action:defaction}/{id:1}/{idparent:2}'); 415 | 416 | // url: /dummy/10/20 =>(controller: dummy, id=10, idparent=20) 417 | // url: /myapi/otherfolder/dummy/10/20 =>(controller: dummy, id=10, idparent=20) 418 | ``` 419 | 420 | > You can define different paths, however it only uses the first part of the path that matches some URL. 421 | > 'path/somepath/{id}' will work 422 | > 'path/{id}/other' will not work 423 | 424 | ### fetchPath() 425 | 426 | Syntax: 427 | 428 | > fetchPath() 429 | 430 | It fetches the path previously defined by addPath, and it returns the name(or number) of the path. If not found, then it returns false 431 | 432 | Example: 433 | 434 | ```php 435 | $route=new RouteOne('http://www.example.dom'); 436 | $route->addPath('{controller}/{id}/{idparent}','optionalname'); 437 | // if the url is : http://www.example.dom/customer/1/200 then it will return 438 | echo $route->fetchPath(); // optionalname 439 | echo $route->controller; // customer 440 | echo $route->id; // 1 441 | echo $route->idparent; // 200 442 | 443 | ``` 444 | 445 | 446 | ## Methods 447 | 448 | ### __construct($base='', $forcedType=null, $isModule=false) 449 | 450 | * string $base base url 451 | * string $forcedType=['api','ws','controller','front']\[$i]
452 | api then it expects a path as api/controller/action/id/idparent
453 | ws then it expects a path as ws/controller/action/id/idparent
454 | controller then it expects a path as controller/action/id/idparent
455 | front then it expects a path as /category/subcategory/subsubcategory/id
456 | * bool $isModule if true then the route start reading a module name
457 | false controller/action/id/idparent
458 | true module/controller/action/id/idparent
459 | array if the value is an array then the value is determined if the first part of the path is in the array.
460 | Example ['modulefolder1','modulefolder2']
461 | 462 | ### getQuery($key,$valueIfNotFound=null) 463 | 464 | It gets a query value (URL). 465 | 466 | >Note: This query does not include the values "req","_event" and "_extra" 467 | 468 | Example: 469 | 470 | ```php 471 | // http://localhost/..../?id=hi 472 | $id=$router->getQuery("id"); // hi 473 | $nf=$router->getQuery("something","not found"); // not found 474 | ``` 475 | 476 | ### setQuery($key,$value) 477 | 478 | It sets a query value 479 | 480 | Example: 481 | 482 | ```php 483 | $route->setQuery("id","hi"); 484 | $id=$router->getQuery("id"); // hi 485 | ``` 486 | 487 | 488 | ### fetch 489 | 490 | Sintax: 491 | > fetchPath() 492 | 493 | Fetch the values from the route, and the values are processed. 494 | 495 | ### callObjectEx 496 | 497 | Sintax 498 | > callObjectEx($classStructure, $throwOnError, $method, $methodGet, $methodPost,$arguments,$injectArguments) 499 | 500 | It creates a new instance of an object (for example, a Controller object) and calls the method.
501 | Note: It is an advanced version of this::callObject()
502 | This method uses {} to replace values based in the next variables:
503 | 504 | | Tag | Description | 505 | |------------------|----------------------------------------------------| 506 | | {controller} | The name of the controller | 507 | | {action} | The current action | 508 | | {event} | The current event | 509 | | {type} | The current type of path (ws,controller,front,api) | 510 | | {module} | The current module (if module is active) | 511 | | {id} | The current id | 512 | | {idparent} | The current idparent | 513 | | {category} | The current category | 514 | | {subcategory} | The current subcategory | 515 | | {subsubcategory} | The current subsubcategory | 516 | 517 | Example: 518 | 519 | ```php 520 | // controller example http://somedomain/Customer/Insert/23 521 | $this->callObjectEx('cocacola\controller\{controller}Controller'); 522 | // it calls the method cocacola\controller\Customer::InsertAction(23,'',''); 523 | 524 | // front example: http://somedomain/product/coffee/nescafe/1 525 | $this->callObjectEx('cocacola\controller\{category}Controller' // the class to call 526 | ,false // if error then it throw an error 527 | ,'{subcategory}' // the method to call (get, post or any other method) 528 | ,null // the method to call (method get) 529 | ,null // the method to call (method post) 530 | ,['subsubcategory','id'] // the arguments to call the method 531 | ,['arg1','arg2']); // arguments that will be passed to the constructor of the instance 532 | // it calls the method cocacola\controller\product::coffee('nescafe','1'); 533 | ``` 534 | 535 | Call a method inside an object using the current route. 536 | 537 | **Example:** 538 | 539 | Router: 540 | 541 | ```php 542 | $databaseService=new SomeDatabaseService(); 543 | $route=new RouteOne(); 544 | 545 | $route->callObjectEx('cocacola\controller\{controller}Controller' // the class to call 546 | ,false // if error then it throw an error 547 | ,'{action}Action' // the method to call (get, post or any other method) 548 | ,'{action}Action{verb}' // the method to call (method get) 549 | ,'{action}Action{verb}' // the method to call (method post) 550 | ,['id', 'idparent', 'event'] // the arguments to call the method 551 | ,[$databaseService,$route]); // (optional)arguments that will be passed to the constructor of the instance 552 | 553 | ``` 554 | 555 | Controller: 556 | 557 | ```php 558 | namespace cocacola\controller; 559 | class CustomerController { 560 | protected $databaseService; 561 | protected $route; 562 | public function __construct($databaseService,$route) { 563 | // optional: injecting services 564 | $this->databaseService=$databaseService; 565 | $this->route=$route; 566 | } 567 | // any action GET or POST 568 | public function GreenAction($id="",$idparent="",$event="") { 569 | } 570 | // GET only action (optional) 571 | public function BlueActionGET($id="",$idparent="",$event="") { 572 | // **my code goes here.** 573 | } 574 | // POST only action (optional) 575 | public function YellowActionPOST($id="",$idparent="",$event="") { 576 | // **my code goes here.** 577 | } 578 | // GET only action (optional) 579 | public function RedActionGET($id="",$idparent="",$event="") { 580 | // **my code goes here.** 581 | } 582 | // any action GET or POST 583 | public function RedAction($id="",$idparent="",$event="") { 584 | // **my code goes here.** 585 | } 586 | 587 | } 588 | ``` 589 | Results: 590 | 591 | | url | method called | 592 | |----------------------------------------------------------|---------------------------------------------------| 593 | | http://localhost/Customer/Green (GET) | GreenAction | 594 | | http://localhost/Customer/Green/20/30?_event=click (GET) | GreenAction($id=20, $idparent=30, $event='click') | 595 | | http://localhost/Customer/Green (POST) | GreenAction | 596 | | http://localhost/Customer/Blue (GET) | BlueActionGET | 597 | | http://localhost/Customer/Blue (POST) | ERROR | 598 | | http://localhost/Customer/Yellow (GET) | ERROR | 599 | | http://localhost/Customer/Yellow (POST) | YellowActionPOST | 600 | | http://localhost/Customer/Red (GET) | RedActionGET (It has priority over RedAction) | 601 | | http://localhost/Customer/Red (POST) | RedAction | 602 | | http://localhost/Customer/Orange | ERROR | 603 | 604 | 605 | 606 | ### callFile($fileStructure='%s.php',$throwOnError=true) 607 | 608 | It calls (include) a php file using the current name of the controller 609 | 610 | * **$fileStructure** The current name of the controller. "%s" is the name of the current controller. Example :/Customer/Insert -> calls the file Customer.php 611 | * **throwOnError** if true then it throws an error. If false then it only returns the error message. 612 | 613 | ### getHeader() 614 | 615 | Syntax: 616 | 617 | > getHeader($key, $valueIfNotFound = null) 618 | 619 | It gets the current header (if any). If the value is not found, then it returns $valueIfNotFound. Note, the $key is always converted to uppercase. 620 | 621 | Example: 622 | 623 | ```php 624 | $token=$this->getHeader('token','TOKEN NOT FOUND'); 625 | ``` 626 | 627 | ### getBody() 628 | 629 | Syntax: 630 | 631 | > getBody($jsonDeserialize = false, $asAssociative = true) 632 | 633 | It gets the body of a request. 634 | 635 | Example: 636 | 637 | ```php 638 | $body=$this->getBody(); // '{"id"=>1,"name"=>john}' (as string) 639 | $body=$this->getBody(true); // stdClass {id=>1,name=>john} 640 | $body=$this->getBody(true,true); // ["id"=>1,"name"=>john] 641 | ``` 642 | 643 | ### 644 | 645 | 646 | 647 | ### getCurrentUrl($withoutFilename = true) 648 | 649 | Returns the current base url without traling space, paremters or queries 650 | 651 | > Note: this function relies on $_SERVER['SERVER_NAME'], and it could be modified by the end-user 652 | 653 | ### getCurrentServer() 654 | 655 | It returns the current server without trailing slash. 656 | 657 | ```php 658 | $route->getCurrentServer(); // http://somedomain 659 | ``` 660 | 661 | ### setCurrentServer($serverName) 662 | 663 | It sets the current server name. It is used by getCurrentUrl() and getCurrentServer(). 664 | **Note:** If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'], and it could be modified 665 | by the user. 666 | 667 | ```php 668 | $route->setCurrentServer('localhost'); 669 | $route->setCurrentServer('127.0.0.1'); 670 | $route->setCurrentServer('domain.dom'); 671 | ``` 672 | 673 | ### getUrl($extraQuery = '',$includeQuery=false) 674 | 675 | It gets the (full) url based in the information in the class. 676 | 677 | ```php 678 | $route->getUrl(); // http://somedomain/controller/action/id 679 | $route->getUrl('id=20'); // http://somedomain/controller/action/id?id=20 680 | $route->getUrl('id=20',true); // http://somedomain/controller/action/id?id=20&field=20&field2=40 681 | ``` 682 | 683 | ### url($module,$controller,$action,$id,$idparent) 684 | 685 | It builds an url based in custom values 686 | 687 | ```php 688 | $route->url(null,"Customer","Update",20); // Customer/Update/20 689 | ``` 690 | 691 | ### urlFront($module,$category,$subcategory,$subsubcategory,$id) 692 | 693 | It builds an url (front) based in custom values 694 | 695 | ```php 696 | $route->url(null,"Daily","Milk",20); // Daily/Milk/20 697 | ``` 698 | 699 | ### alwaysWWW($https = false) 700 | 701 | If the subdomain is empty or different to www, then it redirect to www.domain.com.
702 | Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
703 | Note: If this code needs to redirect, then it stops the execution of the code. Usually it must be called at the 704 | top of the code 705 | 706 | ```php 707 | $route->alwaysWWW(); // if the domain is somedomain.dom/url, then it redirects to www.somedomain.dom/url 708 | $route->alwaysWWW(true); // if the domain is http: somedomain.dom/url, then it redirects to https: www.somedomain.dom/url 709 | 710 | ``` 711 | 712 | ### alwaysHTTPS() 713 | 714 | If the page is loaded as http, then it redirects to https. 715 | Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
716 | Note: If this code needs to redirect, then it stops the execution of the code. Usually it must be called at 717 | the top of the code 718 | 719 | ```php 720 | $route->alwaysHTTPS(); // http://somedomain.com ---> https://somedomain.com 721 | $route->alwaysHTTPS(); // http://localhost ---> // http://localhost 722 | $route->alwaysHTTPS(); // http://127.0.0.1 ---> // http://127.0.0.1 723 | $route->alwaysHTTPS(); // http://mypc ---> // http://mypc 724 | ``` 725 | 726 | ### alwaysNakedDomain($https = false) 727 | 728 | If the subdomain is www (example www.domain.dom) then it redirect to a naked domain domain.dom
729 | Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
730 | Note: If this code needs to redirect, then it stops the execution of the code. Usually, 731 | it must be called at the top of the code 732 | 733 | ```php 734 | $route->alwaysNakedDomain(); // if the domain is www.somedomain.dom/url, then it redirects to somedomain.dom/url 735 | $route->alwaysNakedDomain(true); // if the domain is http: www.somedomain.dom/url, then it redirects to https: somedomain.dom/url 736 | 737 | ``` 738 | 739 | ## fields 740 | 741 | | Field | path | Description | Example | 742 | |-----------------|------------------|--------------------------------------------------------------------------|-------------------------------------------------------------------------| 743 | | $argumentName | | The name of the argument used by Apache .Htaccess and nginx | $this-argumentName='req'; | 744 | | $base | | It is the base url. | $this->base=0; | 745 | | $type | | It is the type of url (api,ws,controller or front) | echo $this->type; // api | 746 | | $module | {module} | It's the current module | echo $this->module; | 747 | | $controller | {controller} | It's the controller. | echo $this->controller; | 748 | | $action | {action} | It's the action. | echo $this->action; | 749 | | $id | {id} | It's the identifier | echo $this->id; | 750 | | $event | {event} | It's the event (such as "click on button). | echo$this->event; | 751 | | $idparent | {idparent} | It is the current parent id (if any) | echo $this->idparent; | 752 | | $extra | {extra} | It's the event (such as "click on button) | echo $this->extra; | 753 | | $category | {category} | The current category. It is useful for the type 'front' | echo $this->category; | 754 | | $subcategory | {subcategory} | The current sub-category. It is useful for the type 'front' | echo $this->subcategory; | 755 | | $subsubcategory | {subsubcategory} | The current sub-sub-category. It is useful for the type 'front' | echo $this->subsubcategory; | 756 | | $identify | | It is an associative array that helps to identify the api and ws route. | $this->identify=['api'=>'apiurl','ws'=>'webservices','controller'=>'']; | 757 | | $isPostBack | | its true if the page is POST, otherwise false. | if ($this->isPostBack) { ... }; | 758 | | $verb | {verb} | The current verb, it could be GET,POST,PUT and DELETE. | if ($this->verb) { ... }; | 759 | 760 | Example: 761 | ```php 762 | $this->addPath('portal/web/{controller}/{action:list}'); 763 | $this->fetchPath(); 764 | var_dump($this-action); // it shows the current action or the default value "list" if none. 765 | ``` 766 | 767 | ## Whitelist 768 | 769 | | Field | Description | Example | 770 | |----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------| 771 | | $allowedVerbs | A list with allowed verbs | $this->allowedVerbs=['GET', 'POST', 'PUT', 'DELETE']; | 772 | | $allowedFields | A list with allowed fields used by **callObjectEx()** | $this->allowedFields=['controller', 'action', 'verb', 'event', 'type', 'module', 'id'
, 'idparent','category', 'subcategory', 'subsubcategory']; | 773 | | setWhitelist() | It sets an associative array with the whitelist to **controller**, **action**, **category**, **subcategory**, **subsubcategory** and **module**.
If not set (null default value), then it allows any entry.
Currently it only work with **controller** and **category** | $this->setWhitelist('controller','Purchase','Invoice','Customer');
$this->setWhitelist('controller',null) // allows any controller; | 774 | 775 | ### Whitelist input. 776 | Whitelisting a method allows two operations: 777 | 778 | * To whitelist an input, for example, only allowing "controllers" that they are inside a list. 779 | * Also, it allows to define the case of an element. 780 | 781 | For example: 782 | 783 | ```php 784 | // Example, value not in the whitelist: someweb.dom/customer/list 785 | $this->setWhiteList('controller',['Product','Client']); 786 | $this->fetch(); 787 | var_dump($this->controller); // null or the default value 788 | var_dump($this->notAllowed); // true (whitelist error) 789 | 790 | 791 | // Example, value in the whitelist but with the wrong case: someweb.dom/customer/list 792 | $this->setWhiteList('controller',['Customer']); 793 | $this->fetch(); 794 | var_dump($this->controller); // it shows "Customer" instead of "customer" 795 | var_dump($this->notAllowed); // false (not error with the validation of the whitelist) 796 | 797 | // reset whitelist for controllers 798 | $this->setWhiteList('controller',null); 799 | ``` 800 | ## CLI 801 | Routeone contains a basic CLI to create and initialize the configuration. 802 | The binary **routeonecli** is located in the vendor/bin folder 803 | 804 | ![docs/cli1.jpg](docs/cli1.jpg) 805 | 806 | * Execute **routeonecli** and it will show a menu with a simple option [router] 807 | 808 | ```shell 809 | ./vendor/bin/routeonecli 810 | ``` 811 | 812 | 813 | 814 | * enter **router** and press enter. 815 | 816 | * You can use the TAB key to autocomplete. 817 | * You can use arrow-up and arrow-down to navigate in the options available 818 | * If you write ?, it will show a simple help 819 | * If you write ??, it will show a more technical help. 820 | 821 | In the router menu, it will show the next screen: 822 | 823 | ![docs/cli2.jpg](docs/cli2.jpg) 824 | 825 | Pending means that the operation is pending to do, or it requires something to configure. 826 | 827 | * enter **configure** 828 | 829 | ![docs/cli3.jpg](docs/cli3.jpg) 830 | 831 | * And enters the router filename without extension. 832 | * Then your developer machine. If you don't know, then press enter. 833 | * Then your developer base url. 834 | * And your production base url. If you don't have or don't know, then just use the default values 835 | 836 | Once done, configure will be marked as "ok" 837 | 838 | Now, lets configure the paths 839 | 840 | * Go to paths. 841 | 842 | ![docs/cli4.jpg](docs/cli4.jpg) 843 | 844 | * And it will show the next menu, add, remove or edit. 845 | * Enter **add** 846 | 847 | ![docs/cli5.jpg](docs/cli5.jpg) 848 | 849 | * And enter the name of the path, example web. 850 | * And the path (check path for see the syntax of the path) 851 | * Finally, enter the name space associate with this path 852 | * Once done, you have your first path. You can add more paths later. 853 | * Press enter to return. 854 | 855 | ![docs/cli6.jpg](docs/cli6.jpg) 856 | 857 | * Now, you can generate the htaccess file, the PHP router file, load the configuration, or save the configuration entered here. 858 | * To exit press return and return. 859 | 860 | 861 | ## Changelog 862 | * 2024-12-31 1.34 863 | * updated to PHP 8.4 864 | * 2024-03-02 1.33 865 | * Updating dependency to PHP 7.4. The extended support of PHP 7.2 ended 3 years ago. 866 | * Added more type hinting in the code. 867 | * 2024-01-22 1.32.1 868 | * fixed a problem when the pattern doesn't have values, example "contact/" 869 | * 2024-01-22 1.32 870 | * unit test updated. 871 | * now route file is always called index.php 872 | * fetchPath() considers the field that is required and the field that is optional. 873 | * 2024-01-09 1.31 874 | * 2023-11-13 1.30.1 875 | * fixed a bug with fetch() when the url fetched is null 876 | * updated .htaccess, so it works better with different situations. 877 | * 2023-05-08 1.30 878 | * addPath() now allows to specify a middleware. 879 | * 2023-04-02 1.29 880 | * [RouteOneCli] updated 881 | * new method instance() so we could get a singleton instance using RouteOne::instance(); 882 | * 2023-03-04 1.28 883 | * Added static paths to addPath() 884 | * callObjectEx() now allows any parameter. If the parameter is not a defined value, then it is obtained from the route. 885 | * callObjectEx() now allows named parameter. 886 | * callObjectEx() now allows to pass an instance and callable instead of the name of the class. 887 | * callObjectEx() allows to filter by type of Path. By default, it does not filter value 888 | * 2023-03-04 1.27.1 889 | * Fix a small bug when addPath() add a path that starts with "/". Now, the value is trimmed. 890 | * 2023-02-15 1.27 891 | * Cleanup of the code and documentation. Deprecating old methods 892 | * 2023-02-14 1.26.4 893 | * some bug fixed 894 | * 2023-01-27 1.26.2 895 | * edited composer json (bin) 896 | * 2023-01-27 1.26 897 | * callObject() marked as deprecated, however you still could use it. 898 | * arguments of function now uses type hinting/validation 899 | * addPath() now throws an exception if the path is empty or null. 900 | * new method redirect() 901 | * new CLI. 902 | * 2023-01-26 1.25 903 | * some cleanups 904 | * 2022-03-11 1.24 905 | * **[fix]** fix many problems when the url is null. 906 | * 2022-02-01 1.23 907 | * [new] getRequest(), getPost(),getGet() 908 | * 2022-01-27 1.22 909 | * [new] callObjectEx allows adding arguments to the constructor. 910 | * [new] clearPath() 911 | * [new] addPath() 912 | * [new] fetchPath() 913 | * [new] getHeader() 914 | * [new] getBody() 915 | * 2021-04-24 1.20 916 | * **constructor** Now it is possible to indicates the possible modules in the constructor. 917 | * Many cleanups of the code. 918 | * New field called **$moduleList** including its setter and getters (by default this value is null) 919 | * If **$moduleList** is not null then it is used to determine if the URL is a module or not 920 | * New field called **$moduleStrategy** assigned in the constructor and in the setter and getters (by default this value is 'none') 921 | * 2021-02-26 1.19 922 | * **setWhiteList()** now works with **controller** and **category** 923 | * **setWhiteList()** also works to define the correct proper case of the elements. 924 | * The method **callObjectEx()** allows to define the case. 925 | * 2021-02-26 1.18 926 | * new fields **$verb** (it gets the current verb, example GET, POST, etc.) 927 | * new whitelist elements: 928 | * $allowedVerbs The list of allowed verbs. 929 | * $allowedFields The list of allowed fields used by **callObjectEx()** 930 | * $allowedControllers The list of allowed controllers. If this list is set and the controller is not in the whitelist 931 | , then the controller is set as null 932 | * The method **callObjectEx()** allows to use the verb. The verb is always ucfirst. 933 | * Example $this->callObjectEx('cocacola\controller\{controller}Controller','{action}Action{verb}'); 934 | * 2021-02-16 1.17 935 | * removed all @ and replaced by **isset()**. Since this library is compatible with PHP 5.6, then it doesn't use "??" 936 | operators. 937 | * **setDefaultValues()** trigger an error if it is called after fetch() 938 | * 2021-02.11 1.16.1 939 | * fixed a problem with "api" and "ws" that it doesn't read the controller in the right position. 940 | * 2021-02-11 1.16 941 | * Removed Travis. 942 | * Lowered the requirement. Now, this library works in PHP 5.6 and higher (instead of PHP 7.0 and higher) 943 | * Constructor has a new argument, it could fetch() the values 944 | * alwaysHTTPS() has a new argument that it could return the full URL (if it requires redirect) or null 945 | * alwaysWWW() has a new argument that it could return the full URL (if it requires redirect) or null 946 | * alwaysNakedDomain() has a new argument that it could return the full URL (if it requires redirect) or null 947 | * 2020-06-14 1.15 948 | * Added default values in setDefaultValues(). 949 | * Method fetch() now it unset the value. 950 | * Fixed Method url(). 951 | * 2020-06-07 1.14.2 952 | * Bug fixed: Delete an echo (used for debug) 953 | * 2020-06-07 1.14.1 954 | * Solved a small bug. it keeps the compatibility. 955 | * 2020-06-07 1.14 956 | * added defcategory,defsubcategory and defsubsubcategory 957 | * new method setIdentifyType() 958 | * 2020-04-23 1.13 959 | * Lots of cleanups. 960 | * 2020-04-04 1.12 961 | * added support for nginx. 962 | * updated the documentation for .htaccess 963 | * new method setCurrentServer() 964 | * 2020-03-27 1.11 965 | * added alwaysNakedDomain() 966 | * 2020-03-27 1.10.1 967 | * a small fix for alwaysHTTPS() 968 | * 2020-03-27 1.10 969 | * added method alwaysHTTPS() and alwaysWWW() 970 | * 2020-02-15 1.9 971 | * added new arguments to callObject() 972 | * new method callObjectEx() 973 | * 2020-02-03 1.8 974 | * new method getNonRouteUrl() 975 | * new method setExtra() 976 | * new method isPostBack() 977 | * new method setIsPostBack() 978 | * Some fixes for getUrl() 979 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eftec/routeone", 3 | "description": "Router service class for PHP", 4 | "type": "library", 5 | "keywords": [ 6 | "php", 7 | "route", 8 | "router", 9 | "mvc" 10 | ], 11 | "homepage": "https://github.com/EFTEC/RouteOne", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Jorge Patricio Castro Castillo", 16 | "email": "jcastro@eftec.cl" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=7.4", 21 | "ext-json": "*", 22 | "eftec/clione": "^1.32" 23 | }, 24 | "archive": { 25 | "exclude": [ 26 | "/examples" 27 | ] 28 | }, 29 | "bin": [ 30 | "lib/routeonecli" 31 | ], 32 | "autoload": { 33 | "psr-4": { 34 | "eftec\\routeone\\": "lib/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "eftec\\tests\\": "tests/" 40 | } 41 | }, 42 | "require-dev": { 43 | "phpunit/phpunit": "^8.5.33" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lib/RouteOne.php: -------------------------------------------------------------------------------- 1 | 'api', 'ws' => 'ws', 'controller' => '']; 84 | /** @var array[] it holds the whitelist. Ex: ['controller'=>['a1','a2','a3']] */ 85 | protected array $whitelist = [ 86 | 'controller' => null, 87 | 'category' => null, 88 | 'action' => null, 89 | 'subcategory' => null, 90 | 'subsubcategory' => null, 91 | 'module' => null 92 | ]; 93 | protected array $whitelistLower = [ 94 | 'controller' => null, 95 | 'category' => null, 96 | 'action' => null, 97 | 'subcategory' => null, 98 | 'subsubcategory' => null, 99 | 'module' => null 100 | ]; 101 | /** 102 | * @var string|null=['api','ws','controller','front'][$i] 103 | */ 104 | private ?string $forceType = ''; 105 | private string $defController = ''; 106 | private string $defAction = ''; 107 | private string $defCategory = ''; 108 | private string $defSubCategory = ''; 109 | private string $defSubSubCategory = ''; 110 | private string $defModule = ''; 111 | /** @var array|bool it stores the list of path used for the modules */ 112 | private $moduleList; 113 | private bool $isModule; 114 | /** 115 | * 122 | * @var string=['none','modulefront','nomodulefront'][$i] 123 | */ 124 | private string $moduleStrategy; 125 | private bool $isFetched = false; 126 | 127 | /** 128 | * RouteOne constructor. 129 | * 130 | * @param string $base base url with or without trailing slash (it's removed if its set).
131 | * Example: ".","http://domain.dom", "http://domain.dom/subdomain"
132 | * @param string|null $forcedType =['api','ws','controller','front'][$i]
133 | * api then it expects a path as api/controller/action/id/idparent
134 | * ws then it expects a path as ws/controller/action/id/idparent
135 | * controller then it expects a path as controller/action/id/idparent
136 | * front then it expects a path as /category/subc/subsubc/id
137 | * @param bool|array $isModule if true then the route start reading a module name
138 | * false controller/action/id/idparent
139 | * true module/controller/action/id/idparent
140 | * array if the value is an array then the value is determined if the 141 | * first 142 | * part of the path is in the array. Example 143 | * ['modulefolder1','modulefolder2']
144 | * @param bool $fetchValues (default false), if true then it also calls the method fetch() 145 | * @param string $moduleStrategy =['none','modulefront','nomodulefront'][$i]
146 | * it changes the strategy to determine the type of url determined if the path 147 | * has a module or not.
148 | * $forcedType must be null, otherwise this value is not calculated.
149 | * 159 | */ 160 | public function __construct(string $base = '', ?string $forcedType = null, $isModule = false, 161 | bool $fetchValues = false, string $moduleStrategy = 'none') 162 | { 163 | $this->base = rtrim($base, '/'); 164 | $this->forceType = $forcedType; 165 | $this->moduleStrategy = $moduleStrategy; 166 | if ($forcedType !== null) { 167 | $this->type = $forcedType; 168 | } 169 | if (is_bool($isModule)) { 170 | $this->moduleList = null; 171 | $this->isModule = $isModule; 172 | } else { 173 | $this->moduleList = $isModule; 174 | $this->isModule = false; 175 | } 176 | $this->isPostBack = false; 177 | if (isset($_SERVER['REQUEST_METHOD']) && in_array($_SERVER['REQUEST_METHOD'], $this->allowedVerbs, true)) { 178 | if ($_SERVER['REQUEST_METHOD'] === 'POST') { 179 | $this->isPostBack = true; 180 | } 181 | $this->verb = $_SERVER['REQUEST_METHOD']; 182 | } else { 183 | $this->verb = 'GET'; 184 | } 185 | $this->setDefaultValues(); 186 | if ($fetchValues) { 187 | $this->fetch(); 188 | } 189 | } 190 | 191 | /** 192 | * It gets the current instance of the library.
193 | * If the instance does not exist, then it is created
194 | * See constructor for the definition of the arguments. 195 | * @param string $base 196 | * @param string|null $forcedType 197 | * @param bool|array $isModule 198 | * @param bool $fetchValues 199 | * @param string $moduleStrategy 200 | * @return RouteOne 201 | */ 202 | public static function instance(string $base = '', ?string $forcedType = null, $isModule = false, 203 | bool $fetchValues = false, string $moduleStrategy = 'none'): RouteOne 204 | { 205 | if (self::$instance === null) { 206 | self::$instance = new RouteOne($base, $forcedType, $isModule, $fetchValues, $moduleStrategy); 207 | } 208 | return self::$instance; 209 | } 210 | 211 | /** 212 | * Returns true if there is an instance of CliOne. 213 | * @return bool 214 | */ 215 | public static function hasInstance(): bool 216 | { 217 | return self::$instance !== null; 218 | } 219 | 220 | /** 221 | * It sets the default controller and action (if they are not entered in the route)
222 | * It is uses to set a default route if the value is empty or its missing.
223 | * Note:It must be set before fetch(). 224 | * 225 | * @param string $defController Default Controller 226 | * @param string $defAction Default action 227 | * @param string $defCategory Default category 228 | * @param string $defSubCategory Default subcategory 229 | * @param string $defSubSubCategory The default sub-sub-category 230 | * @param string $defModule The default module. 231 | * @return $this 232 | */ 233 | public function setDefaultValues(string $defController = '', string $defAction = '', string $defCategory = '', 234 | string $defSubCategory = '', string $defSubSubCategory = '', 235 | string $defModule = ''): self 236 | { 237 | if ($this->isFetched) { 238 | throw new RuntimeException("RouteOne: you can't call setDefaultValues() after fetch()"); 239 | } 240 | $this->defController = $defController; 241 | $this->defAction = $defAction; 242 | $this->defCategory = $defCategory; 243 | $this->defSubCategory = $defSubCategory; 244 | $this->defSubSubCategory = $defSubSubCategory; 245 | $this->defModule = $defModule; 246 | return $this; 247 | } 248 | 249 | /** 250 | * It clears all the paths defined 251 | * 252 | * @return void 253 | */ 254 | public function clearPath(): void 255 | { 256 | $this->path = []; 257 | $this->pathBase = []; 258 | $this->middleWare = []; 259 | } 260 | 261 | /** 262 | * It adds a paths that could be evaluated using fetchPath()
263 | * Example:
264 | * ```php 265 | * $this->addPath('api/{controller}/{action}/{id:0}','apipath'); 266 | * $this->addPath('/api/{controller}/{action}/{id:0}/','apipath'); // "/" at the beginner and end are trimmed. 267 | * $this->addPath('{controller}/{action}/{id:0}','webpath'); 268 | * $this->addPath('{controller:root}/{action}/{id:0}','webpath'); // root path using default 269 | * $this->addPath('somepath','namepath', 270 | * function(callable $next,$id=null,$idparent=null,$event=null) { 271 | * echo "middleware\n"; 272 | * $result=$next($id,$idparent,$event); // calling the controller 273 | * echo "endmiddleware\n"; 274 | * return $result; 275 | * }); 276 | * ``` 277 | * Note:
278 | * The first part of the path, before the "{" is used to determine which path will be used.
279 | * Example "path/{controller}" and "path/{controller}/{id}", the system will consider that are the same path 280 | * @param string $path The path, example "aaa/{requiredvalue}/{optionavalue:default}"
281 | * Where default is the optional default value. 282 | * 293 | * @param string|null $name (optional), the name of the path 294 | * @param callable|null $middleWare A callable function used for middleware.
295 | * The first argument of the function must be a callable method
296 | * The next arguments must be the arguments defined by callObjectEx 297 | * (id,idparent,event) 298 | * @return $this 299 | */ 300 | public function addPath(string $path, ?string $name = null, ?callable $middleWare = null): RouteOne 301 | { 302 | if (!$path) { 303 | throw new RuntimeException('Path must not be empty, use a path with a default value, example: {controller:root}'); 304 | } 305 | $path = trim($path, '/'); 306 | $x0 = strpos($path, '{'); 307 | if ($x0 === false) { 308 | $partStr = ''; 309 | $base = $path; 310 | } else { 311 | $base = substr($path, 0, $x0); // base is the fixed value at the left of the path. 312 | $partStr = substr($path, $x0); 313 | } 314 | $items = explode('/', $partStr); 315 | $itemArr = []; 316 | foreach ($items as $v) { 317 | $p = trim($v, '{}' . " \t\n\r\0\x0B"); 318 | if ($p !== '') { 319 | $itemAdd = explode(':', $p, 2); 320 | if (count($itemAdd) === 1) { 321 | $itemAdd[] = null; // add a default value 322 | } 323 | $itemArr[] = $itemAdd; 324 | } 325 | } 326 | if ($name === null) { 327 | $this->pathBase[] = $base; 328 | $this->path[] = $itemArr; 329 | $this->middleWare[] = $middleWare; 330 | } else { 331 | $this->pathBase[$name] = $base; 332 | $this->path[$name] = $itemArr; 333 | $this->middleWare[$name] = $middleWare; 334 | } 335 | return $this; 336 | } 337 | 338 | /** 339 | * It fetches the path previously defined by addPath. 340 | * @param string $charactersAllowed =['alphanumerichypens','alphanumericnohypens',''][$i] 341 | * 342 | * @return int|string|null return null if not path is evaluated,
343 | * otherwise, it returns the number/name of the path. It could return the value 0 (first path) 344 | */ 345 | public function fetchPath(string $charactersAllowed = 'alphanumericnohypens') 346 | { 347 | $this->lastError = []; 348 | $this->currentPath = null; 349 | $urlFetchedOriginal = $this->getUrlFetchedOriginal(); 350 | $this->queries = $_GET; 351 | $this->event = $this->getRequest('_event'); 352 | $this->extra = $this->getRequest('_extra'); 353 | unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); 354 | foreach ($this->path as $pnum => $pattern) { 355 | if ($this->pathBase[$pnum] !== '' && strpos($urlFetchedOriginal ?? '', $this->pathBase[$pnum]) !== 0) { 356 | // basePath url does not match. Basepath is the fixed path before the variable path 357 | $this->lastError[$pnum] = "Pattern [$pnum], base url does not match"; 358 | continue; 359 | } 360 | $urlFetched = substr($urlFetchedOriginal ?? '', strlen($this->pathBase[$pnum])); 361 | // nginx returns a path as /aaa/bbb apache aaa/bbb 362 | if ($urlFetched !== '') { 363 | $urlFetched = ltrim($urlFetched, '/'); 364 | } 365 | $path = $this->getExtracted($urlFetched); 366 | foreach ($this->path[$pnum] as $key => $v) { 367 | if ($v[1] === null) { 368 | if (!array_key_exists($key, $path) || (!isset($path[$key]) || $path[$key] === '')) { 369 | // the field is required but there we don't find any value 370 | $this->lastError[$pnum] = "Pattern [$pnum] required field ($v[0]) not found in url"; 371 | continue 2; 372 | } 373 | $name = $v[0]; 374 | $value = $path[$key]; 375 | } else { 376 | $name = $v[0]; 377 | if (isset($path[$key]) && $path[$key]) { 378 | $value = $path[$key]; 379 | } else { 380 | // value not found, set default value 381 | $value = $v[1]; 382 | } 383 | } 384 | switch ($charactersAllowed) { 385 | case 'alphanumerichypens': 386 | $pattern = '/[^a-zA-Z0-9_-]/'; 387 | break; 388 | case 'alphanumericnohypens': 389 | $pattern = '/[^a-zA-Z0-9_]/'; 390 | break; 391 | default: 392 | $pattern = null; // no control 393 | } 394 | switch ($name) { 395 | case 'controller': 396 | $this->controller = !$pattern ? $value : preg_replace($pattern, "", $value); 397 | break; 398 | case 'action': 399 | $this->action = !$pattern ? $value : preg_replace($pattern, "", $value); 400 | break; 401 | case 'module': 402 | $this->module = !$pattern ? $value : preg_replace($pattern, "", $value); 403 | break; 404 | case 'id': 405 | $this->id = $value; 406 | break; 407 | case 'idparent': 408 | $this->idparent = $value; 409 | break; 410 | case 'category': 411 | $this->category = !$pattern ? $value : preg_replace($pattern, "", $value); 412 | break; 413 | case 'subcategory': 414 | $this->subcategory = !$pattern ? $value : preg_replace($pattern, "", $value); 415 | break; 416 | case 'subsubcategory': 417 | $this->subsubcategory = !$pattern ? $value : preg_replace($pattern, "", $value); 418 | break; 419 | case '': 420 | break; 421 | default: 422 | throw new RuntimeException("pattern incorrect [$name:$value]"); 423 | } 424 | } 425 | $this->currentPath = $pnum; 426 | break; 427 | } 428 | return $this->currentPath; 429 | } 430 | 431 | /** 432 | * 433 | * It uses the next strategy to obtain the parameters;
434 | * 447 | */ 448 | public function fetch(): void 449 | { 450 | //$urlFetched = $_GET['req'] ?? null; // controller/action/id/.. 451 | $urlFetched = $this->getUrlFetchedOriginal(); // // controller/action/id/.. 452 | $this->isFetched = true; 453 | unset($_GET[$this->argumentName]); 454 | /** @noinspection HostnameSubstitutionInspection */ 455 | $this->httpHost = $_SERVER['HTTP_HOST'] ?? ''; 456 | $this->requestUri = $_SERVER['REQUEST_URI'] ?? ''; 457 | // nginx returns a path as /aaa/bbb apache aaa/bbb 458 | if ($urlFetched !== '') { 459 | $urlFetched = ltrim($urlFetched ?? '', '/'); 460 | } 461 | $this->queries = $_GET; 462 | unset($this->queries[$this->argumentName], $this->queries['_event'], $this->queries['_extra']); 463 | $path = $this->getExtracted($urlFetched, true); 464 | //$first = $path[0] ?? $this->defController; 465 | if (isset($path[0]) && $this->moduleList !== null) { 466 | // if moduleArray has values then we find if the current path is a module or not. 467 | $this->isModule = in_array($path[0], $this->moduleList, true); 468 | } 469 | $id = 0; 470 | if ($this->isModule) { 471 | $this->module = $path[$id] ?? $this->defModule; 472 | $id++; 473 | if ($this->moduleStrategy === 'modulefront') { 474 | // the path is not a module, then type is set as front. 475 | $this->type = 'front'; 476 | } 477 | } else { 478 | $this->module = $this->defModule; 479 | if ($this->moduleStrategy === 'nomodulefront') { 480 | // the path is not a module, then type is set as front. 481 | $this->type = 'front'; 482 | } 483 | } 484 | if ($this->forceType !== null) { 485 | $this->type = $this->forceType; 486 | } 487 | if (!$this->type) { 488 | $this->type = 'controller'; 489 | $this->setController((!$path[$id]) ? $this->defController : $path[$id]); 490 | $id++; 491 | } 492 | switch ($this->type) { 493 | case 'ws': 494 | case 'api': 495 | //$id++; [fixed] 496 | $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); 497 | $id++; 498 | break; 499 | case 'controller': 500 | $this->setController(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defController); 501 | $id++; 502 | break; 503 | case 'front': 504 | // it is processed differently. 505 | $this->setCategory(isset($path[$id]) && $path[$id] ? $path[$id] : $this->defCategory); 506 | $id++; 507 | $this->subcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubCategory; 508 | $id++; 509 | $this->subsubcategory = isset($path[$id]) && $path[$id] ? $path[$id] : $this->defSubSubCategory; 510 | /** @noinspection PhpUnusedLocalVariableInspection */ 511 | $id++; 512 | $this->id = end($path); // id is the last element of the path 513 | $this->event = $this->getRequest('_event'); 514 | $this->extra = $this->getRequest('_extra'); 515 | return; 516 | } 517 | $this->action = $path[$id] ?? null; 518 | $id++; 519 | $this->action = $this->action ?: $this->defAction; // $this->action is never undefined, so we don't need isset 520 | $this->id = $path[$id] ?? null; 521 | $id++; 522 | $this->idparent = $path[$id] ?? null; 523 | /** @noinspection PhpUnusedLocalVariableInspection */ 524 | $id++; 525 | $this->event = $this->getRequest('_event'); 526 | $this->extra = $this->getRequest('_extra'); 527 | } 528 | 529 | /** 530 | * @param string $search 531 | * @param array|string $replace 532 | * @param string $subject 533 | * @param int $limit 534 | * @return string 535 | */ 536 | protected function str_replace_ex(string $search, $replace, string $subject, int $limit = 99999): string 537 | { 538 | return implode($replace, explode($search, $subject, $limit + 1)); 539 | } 540 | 541 | /** 542 | * It is an associative array with the allowed paths or null (default behaviour) to allows any path.
543 | * The comparison ignores cases but the usage is "case-sensitive" and it uses the case used here
544 | * For example: if we allowed the controller called "Controller1" then:
545 | * 549 | * Example: 550 | * ```php 551 | * // we only want to allow the controllers called Purchase, Invoice and Customer. 552 | * $this->setWhiteList('controller',['Purchase','Invoice','Customer']); 553 | * ``` 554 | * Note: this must be executed before fetch() 555 | * @param string $type =['controller','category','action','subcategory','subsubcategory','module'][$i] 556 | * @param array|null $array if null (default value) then we don't validate the information. 557 | */ 558 | public function setWhiteList(string $type, ?array $array): void 559 | { 560 | if ($this->isFetched && $array !== null) { 561 | throw new RuntimeException("RouteOne: you can't call setWhiteList() after fetch()"); 562 | } 563 | $type = strtolower($type); 564 | $this->whitelist[$type] = $array; 565 | $this->whitelistLower[$type] = is_array($array) ? array_map('strtolower', $array) : null; 566 | } 567 | 568 | /** 569 | * If the subdomain is empty or different to www, then it redirects to www.domain.com.
570 | * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
571 | * Note: If this code needs to redirect, then it stops the execution of the code. Usually, 572 | * it must be called at the top of the code 573 | * 574 | * @param bool $https If true then it also redirects to https 575 | * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new full url 576 | * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
577 | * Otherwise, if $redirect=false, it returns the full url to redirect. 578 | */ 579 | public function alwaysWWW(bool $https = false, bool $redirect = true): ?string 580 | { 581 | $url = $this->httpHost; 582 | //if (strpos($url, '.') === false || ip2long($url)) { 583 | //} 584 | if (strpos($url ?? '', 'www.') === false) { 585 | $location = $this->getLocation($https); 586 | $location .= '//www.' . $url . $this->requestUri; 587 | if ($redirect) { 588 | header('HTTP/1.1 301 Moved Permanently'); 589 | header('Location: ' . $location); 590 | if (http_response_code()) { 591 | die(1); 592 | } 593 | } 594 | return $location; 595 | } 596 | if ($https) { 597 | return $this->alwaysHTTPS(false); 598 | } 599 | return null; 600 | } 601 | 602 | private function getLocation($https): string 603 | { 604 | if ($https) { 605 | $port = $_SERVER['HTTP_PORT'] ?? '443'; 606 | $location = 'https:'; 607 | if ($port !== '443' && $port !== '80') { 608 | $location .= $port; 609 | } 610 | } else { 611 | $port = $_SERVER['HTTP_PORT'] ?? '80'; 612 | $location = 'http:'; 613 | if ($port !== '80') { 614 | $location .= $port; 615 | } 616 | } 617 | return $location; 618 | } 619 | 620 | /** 621 | * If the page is loaded as http, then it redirects to https
622 | * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
623 | * Note: If this code needs to redirect, then it stops the execution of the code. Usually, 624 | * it must be called at the top of the code 625 | * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url 626 | * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
627 | * Otherwise, if $redirect=false, it returns the url to redirect. 628 | */ 629 | public function alwaysHTTPS(bool $redirect = true): ?string 630 | { 631 | if (strpos($this->httpHost, '.') === false || ip2long($this->httpHost)) { 632 | return null; 633 | } 634 | $https = $_SERVER['HTTPS'] ?? ''; 635 | if (empty($https) || $https === 'off') { 636 | $port = $_SERVER['HTTP_PORT'] ?? '443'; 637 | $port = ($port === '443' || $port === '80') ? '' : $port; 638 | $location = 'https:' . $port . '//' . $this->httpHost . $this->requestUri; 639 | if ($redirect) { 640 | header('HTTP/1.1 301 Moved Permanently'); 641 | header('Location: ' . $location); 642 | if (http_response_code()) { 643 | die(1); 644 | } 645 | } 646 | return $location; 647 | } 648 | return null; 649 | } 650 | 651 | /** 652 | * If the subdomain is www (example www.domain.dom) then it redirects to a naked domain "domain.dom"
653 | * Note: It doesn't work with localhost, domain without TLD (netbios) or ip domains. It is on purpose.
654 | * Note: If this code needs to redirect, then we should stop the execution of any other code. Usually, 655 | * it must be called at the top of the code 656 | * 657 | * @param bool $https If true then it also redirects to https 658 | * @param bool $redirect if true (default) then it redirects the header. If false, then it returns the new url 659 | * @return string|null It returns null if the operation failed (no correct url or no need to redirect)
660 | * Otherwise, if $redirect=false, it returns the full url to redirect. 661 | */ 662 | public function alwaysNakedDomain(bool $https = false, bool $redirect = true): ?string 663 | { 664 | $url = $this->httpHost; 665 | if (strpos($url ?? '', 'www.') === 0) { 666 | $host = substr($url, 4); // we remove the www. at first 667 | $location = $this->getLocation($https); 668 | $location .= '//' . $host . $this->requestUri; 669 | if ($redirect) { 670 | header('HTTP/1.1 301 Moved Permanently'); 671 | header('Location: ' . $location); 672 | if (http_response_code()) { 673 | die(1); 674 | } 675 | return ''; 676 | } 677 | return $location; 678 | } 679 | if ($https) { 680 | return $this->alwaysHTTPS(false); 681 | } 682 | return null; 683 | } 684 | 685 | /** 686 | * It creates and object (for example, a Controller object) and calls the method.
687 | * Example: (type controller,api,ws) 688 | * ```php 689 | * $this->callObject('cocacola\controller\%sController'); // %s is replaced by the name of the current controller 690 | * $this->callObject('namespace/%2s/%1sClass'); // it calls namespace/Module/ExampleClass (only if module is able) 691 | * $this->callObject('namespace/%2s/%3s%/%1sClass'); // %3s is for the type of path 692 | * ``` 693 | * Note: The method called should be written as (static or not)
694 | * ```php 695 | * public function *nameaction*Action($id="",$idparent="",$event="") { } 696 | * ``` 697 | * 698 | * @param string $classStructure structure of the class.
699 | * Type=controller,api,ws
700 | * The first %s (or %1s) is the name of the controller.
701 | * The second %s (or %2s) is the name of the module (if any and if 702 | * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. 703 | * controller,api,ws,front)
704 | * Type=front
705 | * The first %s (or %1s) is the name of the category.
706 | * The second %s (or %2s) is the name of the subcategory
707 | * The third %s (or %3s) is the type of the subsubcategory
708 | * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If false then it 709 | * returns the error (if any) 710 | * @param string $method [optional] Default value='%sAction'. The name of the method to call (get/post). 711 | * If method does not exist then it will use $methodGet or $methodPost 712 | * @param string $methodGet [optional] Default value='%sAction'. The name of the method to call (get) but only 713 | * if the method defined by $method is not defined. 714 | * @param string $methodPost [optional] Default value='%sAction'. The name of the method to call (post) but 715 | * only 716 | * if the method defined by $method is not defined. 717 | * @param array $arguments [optional] Default value=['id','idparent','event'] the arguments to pass to the 718 | * function 719 | * 720 | * @return string|null null if the operation was correct, or the message of error if it failed. 721 | * @throws Exception 722 | * @deprecated 723 | * @see self::callObjectEx Use callObjectEx('{controller}Controller'); instead of callObject('%sController'); 724 | */ 725 | public function callObject( 726 | string $classStructure = '%sController', bool $throwOnError = true, 727 | string $method = '%sAction', string $methodGet = '%sActionGet', 728 | string $methodPost = '%sActionPost', 729 | array $arguments = ['id', 'idparent', 'event'] 730 | ): ?string 731 | { 732 | if ($this->notAllowed === true) { 733 | throw new UnexpectedValueException('Input is not allowed'); 734 | } 735 | if ($this->type !== 'front') { 736 | if ($this->controller === null) { 737 | throw new UnexpectedValueException('Controller is not set or it is not allowed'); 738 | } 739 | $op = sprintf($classStructure, $this->controller, $this->module, $this->type); 740 | } else { 741 | $op = sprintf($classStructure, $this->category, $this->subcategory, $this->subsubcategory); 742 | } 743 | if (!class_exists($op)) { 744 | if ($throwOnError) { 745 | throw new RuntimeException("Class $op doesn't exist"); 746 | } 747 | return "Class $op doesn't exist"; 748 | } 749 | try { 750 | $controller = new $op(); 751 | if ($this->type !== 'front') { 752 | $actionRequest = sprintf($method, $this->action); 753 | } else { 754 | /** @noinspection PrintfScanfArgumentsInspection */ 755 | $actionRequest = sprintf($method, $this->subcategory, $this->subsubcategory); 756 | } 757 | $actionGetPost = (!$this->isPostBack) ? sprintf($methodGet, $this->action) 758 | : sprintf($methodPost, $this->action); 759 | } catch (Exception $ex) { 760 | if ($throwOnError) { 761 | throw $ex; 762 | } 763 | return $ex->getMessage(); 764 | } 765 | $args = []; 766 | foreach ($arguments as $a) { 767 | $args[] = $this->{$a}; 768 | } 769 | if (method_exists($controller, $actionRequest)) { 770 | try { 771 | $controller->{$actionRequest}(...$args); 772 | } catch (Exception $ex) { 773 | if ($throwOnError) { 774 | throw $ex; 775 | } 776 | return $ex->getMessage(); 777 | } 778 | } elseif (method_exists($controller, $actionGetPost)) { 779 | try { 780 | $controller->{$actionGetPost}(...$args); 781 | } catch (Exception $ex) { 782 | if ($throwOnError) { 783 | throw $ex; 784 | } 785 | return $ex->getMessage(); 786 | } 787 | } else { 788 | $pb = $this->isPostBack ? '(POST)' : '(GET)'; 789 | $msgError = "Action [$actionRequest or $actionGetPost] $pb not found for class [$op]"; 790 | $msgError = strip_tags($msgError); 791 | if ($throwOnError) { 792 | throw new UnexpectedValueException($msgError); 793 | } 794 | return $msgError; 795 | } 796 | return null; 797 | } 798 | 799 | /** 800 | * Get multiples values, get, post, request, header, etc. 801 | * @param string $key The name of the key to read.
802 | * Body and verb do not use a key. 803 | * @param string $type =['get','post','request','header','body','verb'][$i] 804 | * @param mixed|null $defaultValue the default value if the value is not found.
805 | * It is ignored by body and verb because both always returns a value 806 | * @return false|mixed|string|null 807 | * @throws RuntimeException|JsonException 808 | */ 809 | public function getMultiple(string $key, string $type, $defaultValue = null) 810 | { 811 | switch ($type) { 812 | case 'get': 813 | $r = $this->getQuery($key, $defaultValue); 814 | break; 815 | case 'post': 816 | $r = $this->getPost($key, $defaultValue); 817 | break; 818 | case 'request': 819 | $r = $this->getRequest($key, $defaultValue); 820 | break; 821 | case 'header': 822 | $r = $this->getHeader($key, $defaultValue); 823 | break; 824 | case 'body': 825 | $r = $this->getBody(true); 826 | $r = $r === false ? $defaultValue : $r; 827 | break; 828 | case 'verb': 829 | $r = $this->verb; 830 | break; 831 | default: 832 | throw new RuntimeException("argument incorrect, type [$type] unknown"); 833 | } 834 | return $r; 835 | } 836 | 837 | /** 838 | * It creates and object (for example, a Controller object) and calls the method.
839 | * Note: It is an advanced version of this::callObject()
840 | * This method uses {} to replace values.
841 | * 858 | * Note: You can also convert the case 859 | * 865 | * Example:
866 | * ```php 867 | * // controller example http://somedomain/Customer/Insert/23 868 | * $this->callObjectEx('cola\controller\{controller}Controller'); 869 | * // it calls the method cola\controller\Customer::InsertAction(23,'',''); 870 | * 871 | * $this->callObjectEx('cola\controller\{controller}Controller','{action}Action{verb}'); 872 | * // it calls the method cola\controller\Customer::InsertActionGet(23,'',''); or InsertActionPost, etc. 873 | * 874 | * // front example: http://somedomain/product/coffee/nescafe/1 875 | * $this->callObjectEx('cocacola\controller\{category}Controller',false,'{subcategory}',null 876 | * ,null,['subsubcategory','id']); 877 | * // it calls the method cocacola\controller\product::coffee('nescafe','1'); 878 | * 879 | * // callable instead of a class 880 | * $this->callObjectEx(function($id,$idparent,$event) { echo "hi"; }); 881 | * ``` 882 | * 883 | * @param string|object|callable $classStructure [optional] Default value='{controller}Controller'.
884 | * If classStructure is an string then it must indicate the 885 | * full name of the class including namespaces 886 | * (SomeClassController::class is allowed)
887 | * if $classStructure is an object, 888 | * then it uses the instance of it
889 | * if $classStructure is a callable, then it calls the 890 | * function. The arguments are defined by $arguments
891 | * @param bool $throwOnError [optional] Default:true, if true then it throws an exception. If 892 | * false then it returns the error as a string (if any) 893 | * @param string|null $method [optional] Default value='{action}Action'. The name of the method 894 | * to call 895 | * (get/post). If the method does not exist then it will use 896 | * $methodGet 897 | * (isPostBack=false) or $methodPost (isPostBack=true) 898 | * @param string|null $methodGet [optional] Default value='{action}Action{verb}'. The name of the 899 | * method to call when get 900 | * (get) but only if the method defined by $method is not defined. 901 | * @param string|null $methodPost [optional] Default value='{action}Action{verb}'. The name of the 902 | * method to call 903 | * (post) but only if the method defined by $method is not defined. 904 | * @param array $arguments [optional] Default value=['id','idparent','event']
905 | * Values allowed:'get','post','request','header','body','verb'
906 | * T The arguments to 907 | * pass to the methods and middleware
908 | * Example
909 | * 915 | * @param array $injectArguments [optional] You can inject values into the argument of the 916 | * instance's constructor.
It will do nothing if you pass an 917 | * object as 918 | * $classStructure. 919 | * @param string $onlyPath default is "*"(any path), if set, then this method will only work 920 | * if the path 921 | * (obtained by fetchPath) is the indicated here. 922 | * @return string|null Returns a string with an error or null if not error. 923 | * If $classStructure is callable, then it returns the value of the 924 | * function. 925 | * @throws Exception 926 | */ 927 | public function callObjectEx( 928 | $classStructure = '{controller}Controller', bool $throwOnError = true, 929 | ?string $method = '{action}Action', ?string $methodGet = '{action}Action{verb}', 930 | ?string $methodPost = '{action}Action{verb}', array $arguments = ['id', 'idparent', 'event'], 931 | array $injectArguments = [], 932 | string $onlyPath = '*' 933 | ): ?string 934 | { 935 | if ($onlyPath !== '*' && $this->currentPath !== $onlyPath) { 936 | // This object must be called using a specific path. 937 | return null; 938 | } 939 | if ($this->notAllowed === true) { 940 | throw new UnexpectedValueException('Input method is not allowed', 403); 941 | } 942 | if (is_object($classStructure)) { 943 | $className = get_class($classStructure); 944 | } else if (is_callable($classStructure)) { 945 | $className = '**CALLABLE**'; 946 | } else { 947 | $className = $this->replaceNamed($classStructure); 948 | } 949 | if (!class_exists($className) && $className !== '**CALLABLE**') { 950 | if ($throwOnError) { 951 | throw new RuntimeException("Class $className doesn't exist", 404); 952 | } 953 | return "Class $className doesn't exist"; 954 | } 955 | $args = []; 956 | foreach ($arguments as $keyArg => $valueArg) { 957 | if (in_array($valueArg, $this->allowedFields, true)) { 958 | $args[$keyArg] = $this->{$valueArg}; 959 | } else if (is_string($valueArg) || is_numeric($valueArg)) { 960 | $x = explode(':', $valueArg, 3); // get:fieldname:defaultvalue 961 | if (count($x) < 2) { 962 | $msg = 'RouteOne::callObjectEx, argument incorrect, use type:name:default or a defined name'; 963 | if ($throwOnError) { 964 | throw new RuntimeException($msg); 965 | } 966 | return $msg; 967 | } 968 | try { 969 | $args[$keyArg] = $this->getMultiple($x[1], $x[0], $x[2] ?? null); 970 | } catch (Exception $ex) { 971 | if ($throwOnError) { 972 | throw new RuntimeException($ex->getMessage()); 973 | } 974 | return $ex->getMessage(); 975 | } 976 | } else { 977 | // ['field'=>$someobjectorarray] or [$someobjectorarray] 978 | $args[$keyArg] = $valueArg; 979 | } 980 | } 981 | try { 982 | if (is_callable($classStructure)) { 983 | if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { 984 | return $this->middleWare[$this->currentPath]($classStructure, ...$args); 985 | } 986 | return $classStructure(...$args); 987 | } 988 | if (is_object($classStructure)) { 989 | $controller = $classStructure; 990 | } else if (method_exists($className, 'getInstance')) { 991 | $controller = $className->getInstance(); // try to autowire an instance. 992 | } elseif (method_exists($className, 'instance')) { 993 | $controller = $className->instance(); // try to autowire an instance 994 | } else { 995 | $controller = new $className(...$injectArguments); // try to create a new controller. 996 | } 997 | $actionRequest = $this->replaceNamed($method); 998 | $actionGetPost = (!$this->isPostBack) ? $this->replaceNamed($methodGet) 999 | : $this->replaceNamed($methodPost); 1000 | } catch (Exception $ex) { 1001 | if ($throwOnError) { 1002 | throw $ex; 1003 | } 1004 | return $ex->getMessage(); 1005 | } 1006 | if (method_exists($controller, $actionRequest)) { 1007 | /** @noinspection DuplicatedCode */ 1008 | try { 1009 | //$call = $controller->{$actionRequest}; 1010 | if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { 1011 | return $this->middleWare[$this->currentPath]( 1012 | static function(...$args) use ($controller, $actionRequest) { // it is a wrapper function 1013 | return $controller->{$actionRequest}(...$args); 1014 | } 1015 | , ...$args); 1016 | } 1017 | $controller->{$actionRequest}(...$args); 1018 | } catch (Exception $ex) { 1019 | if ($throwOnError) { 1020 | throw $ex; 1021 | } 1022 | return $ex->getMessage(); 1023 | } 1024 | } elseif (method_exists($controller, $actionGetPost)) { 1025 | /** @noinspection DuplicatedCode */ 1026 | try { 1027 | if ($this->currentPath !== null && $this->middleWare[$this->currentPath] !== null) { 1028 | //return $this->middleWare[$this->currentPath]($call, ...$args); 1029 | return $this->middleWare[$this->currentPath]( 1030 | static function(...$args) use ($controller, $actionGetPost) { // it is a wrapper function 1031 | return $controller->{$actionGetPost}(...$args); 1032 | } 1033 | , ...$args); 1034 | } 1035 | $controller->{$actionGetPost}(...$args); 1036 | } catch (Exception $ex) { 1037 | if ($throwOnError) { 1038 | throw $ex; 1039 | } 1040 | return $ex->getMessage(); 1041 | } 1042 | } else { 1043 | $pb = $this->isPostBack ? '(POST)' : '(GET)'; 1044 | $msgError = "Action ex [$actionRequest or $actionGetPost] $pb not found for class [$className]"; 1045 | $msgError = strip_tags($msgError); 1046 | if ($throwOnError) { 1047 | throw new UnexpectedValueException($msgError, 400); 1048 | } 1049 | return $msgError; 1050 | } 1051 | return null; 1052 | } 1053 | 1054 | /** 1055 | * Return a formatted string like vsprintf() with named placeholders.
1056 | * When a placeholder doesn't have a matching key (it's not in the whitelist $allowedFields), then the value 1057 | * is not modified, and it is returned as is.
1058 | * If the name starts with uc_,lc_,u_,l_ then it is converted into ucfirst,lcfirst,uppercase or lowercase. 1059 | * 1060 | * @param string|null $format 1061 | * 1062 | * @return string 1063 | */ 1064 | private function replaceNamed(?string $format): string 1065 | { 1066 | if ($format === null) { 1067 | return ''; 1068 | } 1069 | return preg_replace_callback("/{(\w+)}/", function($matches) { 1070 | $nameField = $matches[1]; 1071 | $result = ''; 1072 | if (strpos($nameField ?? '', '_') > 0) { 1073 | [$x, $nf] = explode('_', $nameField, 2); 1074 | if (in_array($nf, $this->allowedFields, true) === false) { 1075 | return '{' . $nameField . '}'; 1076 | } 1077 | switch ($x) { 1078 | case 'uc': 1079 | $result = ucfirst(strtolower($this->{$nf})); 1080 | break; 1081 | case 'lc': 1082 | $result = lcfirst(strtoupper($this->{$nf})); 1083 | break; 1084 | case 'u': 1085 | $result = strtoupper($this->{$nf}); 1086 | break; 1087 | case 'l': 1088 | $result = strtolower($this->{$nf}); 1089 | break; 1090 | } 1091 | } else { 1092 | if (in_array($nameField, $this->allowedFields, true) === false) { 1093 | return '{' . $nameField . '}'; 1094 | } 1095 | $result = $this->{$nameField}; 1096 | } 1097 | return $result; 1098 | }, $format); 1099 | } 1100 | 1101 | /** 1102 | * It calls (include) a file using the current controller. 1103 | * 1104 | * @param string $fileStructure It uses sprintf
1105 | * The first %s (or %1s) is the name of the controller.
1106 | * The second %s (or %2s) is the name of the module (if any and if 1107 | * ->isModule=true)
The third %s (or %3s) is the type of the path (i.e. 1108 | * controller,api,ws,front)
Example %s.php => controllername.php
Example 1109 | * %s3s%/%1s.php => controller/controllername.php 1110 | * @param bool $throwOnError 1111 | * 1112 | * @return string|null 1113 | * @throws Exception 1114 | */ 1115 | public function callFile(string $fileStructure = '%s.php', bool $throwOnError = true): ?string 1116 | { 1117 | $op = sprintf($fileStructure, $this->controller, $this->module, $this->type); 1118 | try { 1119 | include $op; 1120 | } catch (Exception $ex) { 1121 | if ($throwOnError) { 1122 | throw $ex; 1123 | } 1124 | return $ex->getMessage(); 1125 | } 1126 | return null; 1127 | } 1128 | 1129 | /** 1130 | * Returns the current base url without traling space, paremters or queries/b
1131 | * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and 1132 | * it could be modified by the user. 1133 | * 1134 | * @param bool $withoutFilename if true then it doesn't include the filename 1135 | * 1136 | * @return string 1137 | */ 1138 | public function getCurrentUrl(bool $withoutFilename = true): string 1139 | { 1140 | $sn = $_SERVER['SCRIPT_NAME'] ?? ''; 1141 | if ($withoutFilename) { 1142 | return dirname($this->getCurrentServer() . $sn); 1143 | } 1144 | return $this->getCurrentServer() . $sn; 1145 | } 1146 | 1147 | /** 1148 | * It returns the current server without trailing slash.
1149 | * Note: If $this->setCurrentServer() is not set, then it uses $_SERVER['SERVER_NAME'] and 1150 | * it could be modified by the user. 1151 | * 1152 | * @return string 1153 | */ 1154 | public function getCurrentServer(): string 1155 | { 1156 | $server_name = $this->serverName ?? $_SERVER['SERVER_NAME'] ?? null; 1157 | $c = filter_var($server_name, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME); 1158 | $server_name = $c ? $server_name : $_SERVER['SERVER_ADDR'] ?? '127.0.0.1'; 1159 | $sp = $_SERVER['SERVER_PORT'] ?? 80; 1160 | $port = !in_array($sp, ['80', '443'], true) ? ':' . $sp : ''; 1161 | $https = $_SERVER['HTTPS'] ?? ''; 1162 | $scheme = !empty($https) && (strtolower($https) === 'on' || $https === '1') ? 'https' : 'http'; 1163 | return $scheme . '://' . $server_name . $port; 1164 | } 1165 | 1166 | /** 1167 | * It sets the current server name. It is used by getCurrentUrl() and getCurrentServer() 1168 | * 1169 | * @param string $serverName Example: "localhost", "127.0.0.1", "www.site.com", etc. 1170 | * 1171 | * @see RouteOne::getCurrentUrl 1172 | * @see RouteOne::getCurrentServer 1173 | */ 1174 | public function setCurrentServer(string $serverName): void 1175 | { 1176 | $this->serverName = $serverName; 1177 | } 1178 | 1179 | /** 1180 | * It sets the values of the route using customer values
1181 | * If the values are null, then it keeps the current values (if any) 1182 | * 1183 | * @param null|string $module Name of the module 1184 | * @param null|string $controller Name of the controller. 1185 | * @param null|string $action Name of the action 1186 | * @param null|string $id Name of the id 1187 | * @param null|string $idparent Name of the idparent 1188 | * @param null|string $category Value of the category 1189 | * @param string|null $subcategory Value of the subcategory 1190 | * @param string|null $subsubcategory Value of the sub-subcategory 1191 | * @return $this 1192 | */ 1193 | public function url( 1194 | ?string $module = null, 1195 | ?string $controller = null, 1196 | ?string $action = null, 1197 | ?string $id = null, 1198 | ?string $idparent = null, 1199 | ?string $category = null, 1200 | ?string $subcategory = null, 1201 | ?string $subsubcategory = null 1202 | ): self 1203 | { 1204 | if ($module !== null) { 1205 | $this->module = $module; 1206 | } 1207 | if ($controller !== null) { 1208 | $this->setController($controller); 1209 | } 1210 | if ($action !== null) { 1211 | $this->action = $action; 1212 | } 1213 | if ($id !== null) { 1214 | $this->id = $id; 1215 | } 1216 | if ($idparent !== null) { 1217 | $this->idparent = $idparent; 1218 | } 1219 | if ($category !== null) { 1220 | $this->category = $category; 1221 | } 1222 | if ($subcategory !== null) { 1223 | $this->subcategory = $subcategory; 1224 | } 1225 | if ($subsubcategory !== null) { 1226 | $this->subsubcategory = $subsubcategory; 1227 | } 1228 | $this->extra = null; 1229 | $this->event = null; 1230 | return $this; 1231 | } 1232 | 1233 | public function urlFront( 1234 | $module = null, 1235 | $category = null, 1236 | $subcategory = null, 1237 | $subsubcategory = null, 1238 | $id = null 1239 | ): RouteOne 1240 | { 1241 | if ($module) { 1242 | $this->module = $module; 1243 | } 1244 | if ($category) { 1245 | $this->setCategory($category); 1246 | } 1247 | if ($subcategory) { 1248 | $this->subcategory = $subcategory; 1249 | } 1250 | if ($subsubcategory) { 1251 | $this->subsubcategory = $subsubcategory; 1252 | } 1253 | if ($id) { 1254 | $this->id = $id; 1255 | } 1256 | $this->extra = null; 1257 | $this->event = null; 1258 | return $this; 1259 | } 1260 | 1261 | // .htaccess: 1262 | // RewriteRule ^(.*)$ index.php?req=$1 [L,QSA] 1263 | public function reset(): RouteOne 1264 | { 1265 | // $this->base=''; base is always keep 1266 | $this->isFetched = false; 1267 | $this->defController = ''; 1268 | $this->defCategory = ''; 1269 | $this->defSubCategory = ''; 1270 | $this->defSubSubCategory = ''; 1271 | $this->defModule = ''; 1272 | $this->forceType = null; 1273 | $this->defAction = ''; 1274 | $this->isModule = ''; 1275 | $this->moduleStrategy = 'none'; 1276 | $this->moduleList = null; 1277 | $this->id = null; 1278 | $this->event = null; 1279 | $this->idparent = null; 1280 | $this->extra = null; 1281 | $this->verb = 'GET'; 1282 | $this->notAllowed = false; 1283 | $this->clearPath(); 1284 | $this->currentPath = null; 1285 | return $this; 1286 | } 1287 | 1288 | /** 1289 | * This function is used to identify the type automatically. If the url is empty then it is marked as default
1290 | * It returns the first one that matches. 1291 | * Example:
1292 | * ```php 1293 | * $this->setIdentifyType([ 1294 | * 'controller' =>'backend', // domain.dom/backend/controller/action => controller type 1295 | * 'api'=>'api', // domain.dom/api/controller => api type 1296 | * 'ws'=>'api/ws', // domain.dom/api/ws/controller => ws type 1297 | * 'front'=>'..']); // domain.dom/* =>front (any other that does not match) 1298 | * ``` 1299 | * 1300 | * @param $array 1301 | */ 1302 | public function setIdentifyType($array): void 1303 | { 1304 | $this->identify = $array; 1305 | } 1306 | 1307 | /** 1308 | * It returns a non route url based in the base url.
1309 | * Example:
1310 | * $this->getNonRouteUrl('login.php'); // http://baseurl.com/login.php 1311 | * 1312 | * @param string $urlPart 1313 | * 1314 | * @return string 1315 | * @see RouteOne 1316 | */ 1317 | public function getNonRouteUrl(string $urlPart): string 1318 | { 1319 | return $this->base . '/' . $urlPart; 1320 | } 1321 | 1322 | /** 1323 | * It reconstructs an url using the current information.
1324 | * Example:
1325 | * ```php 1326 | * $currenturl=$this->getUrl(); 1327 | * $buildurl=$this->url('mod','controller','action',20)->getUrl(); 1328 | * ``` 1329 | * Note:. It discards any information outside the values pre-defined 1330 | * (example: /controller/action/id/idparent/?arg=1&arg=2)
1331 | * It does not consider the path() structure but the type of url. 1332 | * @param string $extraQuery If we want to add extra queries 1333 | * @param bool $includeQuery If true then it includes the queries in $this->queries 1334 | * 1335 | * @return string 1336 | */ 1337 | public function getUrl(string $extraQuery = '', bool $includeQuery = false): string 1338 | { 1339 | $url = $this->base . '/'; 1340 | if ($this->isModule) { 1341 | $url .= $this->module . '/'; 1342 | } 1343 | switch ($this->type) { 1344 | case 'ws': 1345 | case 'controller': 1346 | case 'api': 1347 | $url .= ''; 1348 | break; 1349 | case 'front': 1350 | $url .= "$this->category/$this->subcategory/$this->subsubcategory/"; 1351 | if ($this->id) { 1352 | $url .= $this->id . '/'; 1353 | } 1354 | if ($this->idparent) { 1355 | $url .= $this->idparent . '/'; 1356 | } 1357 | return $url; 1358 | default: 1359 | trigger_error('type [' . $this->type . '] not defined'); 1360 | break; 1361 | } 1362 | $url .= $this->controller . '/'; // Controller is always visible, even if it is empty 1363 | $url .= $this->action . '/'; // action is always visible, even if it is empty 1364 | $sepQuery = '?'; 1365 | if (($this->id !== null && $this->id !== '') || $this->idparent !== null) { 1366 | $url .= $this->id . '/'; // id is visible if id is not empty or if idparent is not empty. 1367 | } 1368 | if ($this->idparent !== null && $this->idparent !== '') { 1369 | $url .= $this->idparent . '/'; // idparent is only visible if it is not empty (zero is not empty) 1370 | } 1371 | if ($this->event !== null && $this->event !== '') { 1372 | $url .= '?_event=' . $this->event; 1373 | $sepQuery = '&'; 1374 | } 1375 | if ($this->extra !== null && $this->extra !== '') { 1376 | $url .= $sepQuery . '_extra=' . $this->extra; 1377 | $sepQuery = '&'; 1378 | } 1379 | if ($extraQuery !== null && $extraQuery !== '') { 1380 | $url .= $sepQuery . $extraQuery; 1381 | $sepQuery = '&'; 1382 | } 1383 | if ($includeQuery && count($this->queries)) { 1384 | $url .= $sepQuery . http_build_query($this->queries); 1385 | } 1386 | return $url; 1387 | } 1388 | 1389 | /** 1390 | * Returns the url using the path and current values
1391 | * The trail "/" is always removed. 1392 | * @param string|null $idPath If null then it uses the current path obtained by fetchUrl()
1393 | * If not null, then it uses the id path to obtain the path. 1394 | * @return string 1395 | */ 1396 | public function getUrlPath(?string $idPath = null): string 1397 | { 1398 | $idPath = $idPath ?? $this->currentPath; 1399 | if (!isset($this->path[$idPath])) { 1400 | throw new RuntimeException("Path $idPath not defined"); 1401 | } 1402 | $patternItems = $this->path[$idPath]; 1403 | $url = $this->base . '/' . $this->pathBase[$idPath]; 1404 | $final = []; 1405 | foreach ($patternItems as $vArr) { 1406 | [$idx, $def] = $vArr; 1407 | $value = $this->{$idx} ?? $def; 1408 | $final[] = $value; 1409 | } 1410 | $url .= implode('/', $final); 1411 | return rtrim($url, '/'); 1412 | } 1413 | 1414 | /** 1415 | * It returns the current type. 1416 | * 1417 | * @return string 1418 | */ 1419 | public function getType(): string 1420 | { 1421 | return $this->type; 1422 | } 1423 | 1424 | /** 1425 | * It returns the current name of the module 1426 | * 1427 | * @return string 1428 | */ 1429 | public function getModule(): string 1430 | { 1431 | return $this->module; 1432 | } 1433 | 1434 | /** 1435 | * @param string $key 1436 | * @param null|mixed $valueIfNotFound 1437 | * 1438 | * @return mixed 1439 | */ 1440 | public function getQuery(string $key, $valueIfNotFound = null) 1441 | { 1442 | return $this->queries[$key] ?? $valueIfNotFound; 1443 | } 1444 | 1445 | /** 1446 | * It gets the current header (if any) 1447 | * 1448 | * @param string $key The key to read 1449 | * @param null|mixed $valueIfNotFound 1450 | * @return mixed|null 1451 | */ 1452 | public function getHeader(string $key, $valueIfNotFound = null) 1453 | { 1454 | $keyname = 'HTTP_' . strtoupper($key); 1455 | return $_SERVER[$keyname] ?? $valueIfNotFound; 1456 | } 1457 | 1458 | /** 1459 | * It gets the Post value if not the Get value 1460 | * 1461 | * @param string $key The key to read 1462 | * @param null|mixed $valueIfNotFound 1463 | * @return mixed|null 1464 | */ 1465 | public function getRequest(string $key, $valueIfNotFound = null) 1466 | { 1467 | return $_POST[$key] ?? $_GET[$key] ?? $valueIfNotFound; 1468 | } 1469 | 1470 | /** 1471 | * It gets the Post value or returns the default value if not found 1472 | * 1473 | * @param string $key The key to read 1474 | * @param null|mixed $valueIfNotFound 1475 | * @return mixed|null 1476 | */ 1477 | public function getPost(string $key, $valueIfNotFound = null) 1478 | { 1479 | return $_POST[$key] ?? $valueIfNotFound; 1480 | } 1481 | 1482 | /** 1483 | * It gets the Get (url parameter) value or returns the default value if not found 1484 | * 1485 | * @param string $key The key to read 1486 | * @param null|mixed $valueIfNotFound 1487 | * @return mixed|null 1488 | */ 1489 | public function getGet(string $key, $valueIfNotFound = null) 1490 | { 1491 | return $_GET[$key] ?? $valueIfNotFound; 1492 | } 1493 | 1494 | /** 1495 | * It gets the body of a request. 1496 | * 1497 | * @param bool $jsonDeserialize if true then it de-serialize the values. 1498 | * @param bool $asAssociative if true (default value) then it returns as an associative array. 1499 | * @return false|mixed|string 1500 | * @throws JsonException 1501 | */ 1502 | public function getBody(bool $jsonDeserialize = false, bool $asAssociative = true) 1503 | { 1504 | $entityBody = file_get_contents('php://input'); 1505 | if (!$jsonDeserialize) { 1506 | return $entityBody; 1507 | } 1508 | return json_decode($entityBody, $asAssociative, 512, JSON_THROW_ON_ERROR); 1509 | } 1510 | 1511 | /** 1512 | * It sets a query value 1513 | * 1514 | * @param string $key 1515 | * @param null|mixed $value 1516 | */ 1517 | public function setQuery(string $key, $value): void 1518 | { 1519 | $this->queries[$key] = $value; 1520 | } 1521 | 1522 | /** 1523 | * It returns the current name of the controller. 1524 | * 1525 | * @return string|null 1526 | */ 1527 | public function getController(): ?string 1528 | { 1529 | return $this->controller; 1530 | } 1531 | 1532 | /** 1533 | * 1534 | * @param $controller 1535 | * 1536 | * @return RouteOne 1537 | */ 1538 | public function setController($controller): RouteOne 1539 | { 1540 | if (is_array($this->whitelist['controller'])) { // there is a whitelist 1541 | if (in_array(strtolower($controller), $this->whitelistLower['controller'], true)) { 1542 | $p = array_search($controller, $this->whitelistLower['controller'], true); 1543 | $this->controller = $this->whitelist['controller'][$p]; // we returned the same value but with the right case. 1544 | return $this; 1545 | } 1546 | // and this value is not found there. 1547 | $this->controller = $this->defController; 1548 | $this->notAllowed = true; 1549 | return $this; 1550 | } 1551 | $this->controller = $controller; 1552 | return $this; 1553 | } 1554 | 1555 | /** 1556 | * 1557 | * 1558 | * @return string|null 1559 | */ 1560 | public function getAction(): ?string 1561 | { 1562 | return $this->action; 1563 | } 1564 | 1565 | /** 1566 | * 1567 | * 1568 | * @param $action 1569 | * 1570 | * @return RouteOne 1571 | */ 1572 | public function setAction($action): RouteOne 1573 | { 1574 | $this->action = $action; 1575 | return $this; 1576 | } 1577 | 1578 | /** 1579 | * 1580 | * 1581 | * @return string 1582 | */ 1583 | public function getId(): string 1584 | { 1585 | return $this->id; 1586 | } 1587 | 1588 | /** 1589 | * 1590 | * 1591 | * @param $id 1592 | * 1593 | * @return RouteOne 1594 | */ 1595 | public function setId($id): RouteOne 1596 | { 1597 | $this->id = $id; 1598 | return $this; 1599 | } 1600 | 1601 | /** 1602 | * 1603 | * 1604 | * @return string 1605 | */ 1606 | public function getEvent(): string 1607 | { 1608 | return $this->event; 1609 | } 1610 | 1611 | /** 1612 | * 1613 | * 1614 | * @param $event 1615 | * 1616 | * @return RouteOne 1617 | */ 1618 | public function setEvent($event): RouteOne 1619 | { 1620 | $this->event = $event; 1621 | return $this; 1622 | } 1623 | 1624 | /** 1625 | * 1626 | * 1627 | * @return string|null 1628 | */ 1629 | public function getIdparent(): ?string 1630 | { 1631 | return $this->idparent; 1632 | } 1633 | 1634 | /** 1635 | * @param $idParent 1636 | * 1637 | * @return RouteOne 1638 | */ 1639 | public function setIdParent($idParent): RouteOne 1640 | { 1641 | $this->idparent = $idParent; 1642 | return $this; 1643 | } 1644 | 1645 | /** 1646 | * 1647 | * 1648 | * @return string 1649 | */ 1650 | public function getExtra(): string 1651 | { 1652 | return $this->extra; 1653 | } 1654 | 1655 | /** 1656 | * @param string $extra 1657 | * 1658 | * @return RouteOne 1659 | */ 1660 | public function setExtra(string $extra): RouteOne 1661 | { 1662 | $this->extra = $extra; 1663 | return $this; 1664 | } 1665 | 1666 | /** 1667 | * It gets the current category 1668 | * 1669 | * @return string|null 1670 | */ 1671 | public function getCategory(): ?string 1672 | { 1673 | return $this->category; 1674 | } 1675 | 1676 | /** 1677 | * It sets the current category 1678 | * 1679 | * @param string $category 1680 | * 1681 | * @return RouteOne 1682 | */ 1683 | public function setCategory(string $category): RouteOne 1684 | { 1685 | if (is_array($this->whitelist['category'])) { // there is a whitelist 1686 | if (in_array(strtolower($category), $this->whitelistLower['category'], true)) { 1687 | $p = array_search($category, $this->whitelistLower['category'], true); 1688 | $this->category = $this->whitelist['category'][$p]; // we returned the same value but with the right case. 1689 | return $this; 1690 | } 1691 | // and this value is not found there. 1692 | $this->category = $this->defCategory; 1693 | $this->notAllowed = true; 1694 | return $this; 1695 | } 1696 | $this->category = $category; 1697 | return $this; 1698 | } 1699 | 1700 | /** 1701 | * It gets the current sub category 1702 | * 1703 | * @return string 1704 | */ 1705 | public function getSubcategory(): string 1706 | { 1707 | return $this->subcategory; 1708 | } 1709 | 1710 | /** 1711 | * It gets the current sub-sub-category 1712 | * 1713 | * @return string 1714 | */ 1715 | public function getSubsubcategory(): string 1716 | { 1717 | return $this->subsubcategory; 1718 | } 1719 | 1720 | /** 1721 | * Returns true if the current web method is POST. 1722 | * 1723 | * @return bool 1724 | */ 1725 | public function isPostBack(): bool 1726 | { 1727 | return $this->isPostBack; 1728 | } 1729 | 1730 | /** 1731 | * It sets if the current state is postback 1732 | * 1733 | * @param bool $isPostBack 1734 | * 1735 | * @return RouteOne 1736 | */ 1737 | public function setIsPostBack(bool $isPostBack): RouteOne 1738 | { 1739 | $this->isPostBack = $isPostBack; 1740 | return $this; 1741 | } 1742 | 1743 | /** 1744 | * It gets the current list of module lists or null if there is none. 1745 | * 1746 | * @return array|bool 1747 | * @noinspection PhpUnused 1748 | */ 1749 | public function getModuleList() 1750 | { 1751 | return $this->moduleList; 1752 | } 1753 | 1754 | /** 1755 | * It sets the current list of modules or null to assigns nothing. 1756 | * 1757 | * @param array|bool $moduleList 1758 | * @noinspection PhpUnused 1759 | * 1760 | * @return RouteOne 1761 | */ 1762 | public function setModuleList($moduleList): RouteOne 1763 | { 1764 | $this->moduleList = $moduleList; 1765 | return $this; 1766 | } 1767 | 1768 | /** 1769 | * It gets the current strategy of module. 1770 | * 1771 | * @return string=['none','modulefront','nomodulefront'][$i] 1772 | * @see RouteOne::setModuleStrategy 1773 | */ 1774 | public function getModuleStrategy(): string 1775 | { 1776 | return $this->moduleStrategy; 1777 | } 1778 | 1779 | /** 1780 | * it changes the strategy to determine the type of url determined if the path has a module or not.
1781 | * $forcedType must be null, otherwise this value is not used.
1782 | * 1789 | * @param string $moduleStrategy 1790 | * 1791 | * @return RouteOne 1792 | */ 1793 | public function setModuleStrategy(string $moduleStrategy): RouteOne 1794 | { 1795 | $this->moduleStrategy = $moduleStrategy; 1796 | return $this; 1797 | } 1798 | 1799 | /** 1800 | * @return mixed|null 1801 | */ 1802 | private function getUrlFetchedOriginal() 1803 | { 1804 | $this->notAllowed = false; // reset 1805 | $this->isFetched = true; 1806 | $urlFetchedOriginal = $_GET[$this->argumentName] ?? null; // controller/action/id/.. 1807 | if ($urlFetchedOriginal !== null) { 1808 | $urlFetchedOriginal = rtrim($urlFetchedOriginal, '/'); 1809 | } 1810 | unset($_GET[$this->argumentName]); 1811 | /** @noinspection HostnameSubstitutionInspection */ 1812 | $this->httpHost = isset($_SERVER['HTTP_HOST']) ? filter_var($_SERVER['HTTP_HOST'], FILTER_SANITIZE_URL) : ''; 1813 | $this->requestUri = isset($_SERVER['REQUEST_URI']) ? filter_var($_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL) : ''; 1814 | return $urlFetchedOriginal; 1815 | } 1816 | 1817 | /** 1818 | * @param string $urlFetched 1819 | * @param bool $sanitize 1820 | * @return array 1821 | */ 1822 | private function getExtracted(string $urlFetched, bool $sanitize = false): array 1823 | { 1824 | if ($sanitize) { 1825 | $urlFetched = filter_var($urlFetched, FILTER_SANITIZE_URL); 1826 | } 1827 | if (is_array($this->identify) && $this->type === '') { 1828 | foreach ($this->identify as $ty => $path) { 1829 | if ($path === '') { 1830 | $this->type = $ty; 1831 | break; 1832 | } 1833 | if (strpos($urlFetched ?? '', $path) === 0) { 1834 | $urlFetched = ltrim($this->str_replace_ex($path, '', $urlFetched, 1), '/'); 1835 | $this->type = $ty; 1836 | break; 1837 | } 1838 | } 1839 | } 1840 | return explode('/', $urlFetched); 1841 | } 1842 | 1843 | public function redirect(string $url, int $statusCode = 303): void 1844 | { 1845 | header('Location: ' . $url, true, $statusCode); 1846 | if (http_response_code()) { 1847 | die(1); 1848 | } 1849 | } 1850 | // 1851 | 1852 | /** 1853 | * @param string $key the name of the flag to read 1854 | * @param string|null $default is the default value is the parameter is set 1855 | * without value. 1856 | * @param bool $set it is the value returned when the argument is set but there is no value assigned 1857 | * @return string 1858 | */ 1859 | public static function getParameterCli(string $key, ?string $default = '', bool $set = true) 1860 | { 1861 | global $argv; 1862 | $p = array_search('-' . $key, $argv, true); 1863 | if ($p === false) { 1864 | return $default; 1865 | } 1866 | if (isset($argv[$p + 1])) { 1867 | return self::removeTrailSlash($argv[$p + 1]); 1868 | } 1869 | return $set; 1870 | } 1871 | 1872 | public static function isAbsolutePath($path): bool 1873 | { 1874 | if (!$path) { 1875 | return true; 1876 | } 1877 | if (DIRECTORY_SEPARATOR === '/') { 1878 | // linux and macos 1879 | return $path[0] === '/'; 1880 | } 1881 | return $path[1] === ':'; 1882 | } 1883 | 1884 | protected static function removeTrailSlash($txt): string 1885 | { 1886 | return rtrim($txt, '/\\'); 1887 | } 1888 | // 1889 | } 1890 | -------------------------------------------------------------------------------- /lib/RouteOneCli.php: -------------------------------------------------------------------------------- 1 | route = new RouteOne(); 26 | $this->cli = CliOne::instance(); 27 | $this->cli->debug = true; 28 | if (!CliOne::hasMenu()) { 29 | $this->cli->setErrorType(); 30 | $this->cli->addMenu('mainmenu', 31 | function($cli) { 32 | $cli->upLevel('main menu'); 33 | $cli->setColor(['byellow'])->showBread(); 34 | } 35 | , function(CliOne $cli) { 36 | $cli->downLevel(2); 37 | }); 38 | } 39 | $this->cli->addMenuService('mainmenu', $this); 40 | $this->cli->addMenuItem('mainmenu', 'router', 41 | '[{{routerconfigfull}}] Configure the router', 'navigate:routermenu'); 42 | $this->cli->addMenu('routermenu', 43 | function($cli) { 44 | $cli->upLevel('router menu'); 45 | $cli->setColor(['byellow'])->showBread(); 46 | } 47 | , 'footer'); 48 | /** 49 | * The next comments are used to indicate that the methods indicated are used here. 50 | * @see RouteOneCli::menurouteroneconfigure 51 | * @see RouteOneCli::menurouteronehtaccess 52 | * @see RouteOneCli::menurouteronerouter 53 | * @see RouteOneCli::menurouteronepaths 54 | * @see RouteOneCli::menurouteroneload 55 | * @see RouteOneCli::menurouteronesave 56 | */ 57 | $this->cli->addMenuItems('routermenu', [ 58 | 'configure' => ['[{{routerconfig}}] configure and connect to the database', 'routeroneconfigure'], 59 | 'htaccess' => ['[{{routerconfigfull}}] create the htaccess file', 'routeronehtaccess'], 60 | 'router' => ['[{{routerconfigfull}}] create the PHP router file', 'routeronerouter'], 61 | 'paths' => ['[{{routerconfigpath}}] modify the paths', 'routeronepaths'], 62 | 'load' => [' load the configuration', 'routeroneload'], 63 | 'save' => ['[{{routerconfigpath}}] save the configuration', 'routeronesave'], 64 | ]); 65 | //$this->cli->addMenuItem('pdooneconnect'); 66 | $this->cli->setVariable('routerconfig', 'pending'); 67 | $this->cli->setVariable('routerconfigpath', 'pending'); 68 | $this->cli->setVariable('routerconfigfull', 'pending'); 69 | $this->cli->addVariableCallBack('router', function(CliOne $cli) { 70 | if ($cli->getValue('dev')) { 71 | $file = true; 72 | $cli->setVariable('routerconfig', 'ok', false); 73 | } else { 74 | $file = false; 75 | $cli->setVariable('routerconfig', 'pending', false); 76 | } 77 | if (count($this->paths) > 0) { 78 | $path = true; 79 | $cli->setVariable('routerconfigpath', 'ok', false); 80 | } else { 81 | $path = false; 82 | $cli->setVariable('routerconfigpath', 'pending', false); 83 | } 84 | if ($file && $path) { 85 | $cli->setVariable('routerconfigfull', 'ok', false); 86 | } else { 87 | $cli->setVariable('routerconfigfull', 'pending', false); 88 | } 89 | }); 90 | $listPHPFiles = $this->getFiles('.', '.config.php'); 91 | $routerFileName = $this->cli->createOrReplaceParam('routerfilename', [], 'longflag') 92 | ->setRequired(false) 93 | ->setCurrentAsDefault() 94 | ->setDescription('select a configuration file to load', 'Select the configuration file to use', [ 95 | 'Example: "--routerfilename myconfig"'] 96 | , 'file') 97 | ->setDefault('') 98 | ->setInput(false, 'string', $listPHPFiles) 99 | ->evalParam(); 100 | $this->routerOneLoad($routerFileName); 101 | if ($run) { 102 | if ($this->cli->getSTDIN() === null) { 103 | $this->showLogo(); 104 | } 105 | $this->cli->evalMenu('mainmenu', $this); 106 | } 107 | } 108 | 109 | public function showLogo(): void 110 | { 111 | echo " _____ _ ____ \n"; 112 | echo " | __ \ | | / __ \ \n"; 113 | echo " | |__) |___ _ _| |_ ___| | | |_ __ ___ \n"; 114 | echo " | _ // _ \| | | | __/ _ \ | | | '_ \ / _ \\\n"; 115 | echo " | | \ \ (_) | |_| | || __/ |__| | | | | __/\n"; 116 | echo " |_| \_\___/ \__,_|\__\___|\____/|_| |_|\___| " . self::VERSION . "\n\n"; 117 | echo "\n"; 118 | } 119 | 120 | public function option(): void 121 | { 122 | $this->cli->createOrReplaceParam('init', [], 'command')->add(); 123 | } 124 | 125 | /** 126 | * @throws JsonException 127 | */ 128 | public function menuRouterOnePaths(): void 129 | { 130 | $this->cli->upLevel('paths'); 131 | //$this->cli->setColor(['byellow'])->showBread(); 132 | while (true) { 133 | $this->cli->setColor(['byellow'])->showBread(); 134 | $this->cli->showValuesColumn($this->paths, 'option'); 135 | $ecc = $this->cli->createOrReplaceParam('extracolumncommand') 136 | ->setAllowEmpty() 137 | ->setInput(true, 'optionshort', ['add', 'remove', 'edit']) 138 | ->setDescription('', 'Select an operation') 139 | ->evalParam(true); 140 | switch ($ecc->value) { 141 | case '': 142 | break 2; 143 | case 'add': 144 | $tmp = $this->cli->createOrReplaceParam('selectpath') 145 | //->setAllowEmpty() 146 | ->setInput() 147 | ->setDescription('', 'Select the name of the path', 148 | ['select an unique name for this path', 'example:web']) 149 | ->evalParam(true); 150 | $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') 151 | //->setAllowEmpty() 152 | ->setInput() 153 | ->setDescription('', 'Select the path (? for help)', 154 | ['select the path to be used using the syntax:', 155 | 'fixedpath/{requiredvalue}/{optionalvalue:defaultvalue}', 156 | 'Example:{controller:Home}/{id}/{idparent} ' 157 | , '{controller}: the controller' 158 | , '{action}: the action' 159 | , '{id}: the identifier' 160 | , '{idparent}: the parent object' 161 | , '{category}: the category' 162 | , '{subcategory}: the subcategory' 163 | , '{subsubcategory}: the subsubcategory']) 164 | ->setDefault('{controller:Home}/{action:list}/{id}/{idparent}') 165 | ->evalParam(true); 166 | $tmp3 = $this->cli->createOrReplaceParam('extracolumn_namespace') 167 | //->setAllowEmpty() 168 | ->setInput() 169 | ->setDescription('', 'Select the namespace associate with the path', 170 | ['example: eftec\\controller']) 171 | ->setDefault('eftec\\controller') 172 | ->evalParam(true); 173 | $this->paths[$tmp->value] = $tmp2->value . ', ' . $tmp3->value; 174 | break; 175 | case 'remove': 176 | $tmp = $this->cli->createOrReplaceParam('extracolumn_delete') 177 | ->setAllowEmpty() 178 | ->setInput(true, 'option', $this->paths) 179 | ->setDescription('', 'Select the column to delete') 180 | ->evalParam(true); 181 | if ($tmp->valueKey !== $this->cli->emptyValue) { 182 | unset($this->paths[$tmp->valueKey]); 183 | } 184 | break; 185 | case 'edit': 186 | $tmp = $this->cli->createOrReplaceParam('extracolumn_edit') 187 | ->setAllowEmpty() 188 | ->setInput(true, 'option', $this->paths) 189 | ->setDescription('', 'Select the column to edit') 190 | ->evalParam(true); 191 | if ($tmp->valueKey !== $this->cli->emptyValue) { 192 | $v = explode(', ', $this->paths[$tmp->valueKey], 2); 193 | $tmp2 = $this->cli->createOrReplaceParam('extracolumn_sql') 194 | //->setAllowEmpty() 195 | ->setInput() 196 | ->setDescription('', 'Select the path', 197 | ['select the path to be used using the syntax {id:defaultvalue}', 198 | 'example:{controller:Home}/{id}/{idparent} ' 199 | , '{controller}: the controller' 200 | , '{action}: the action' 201 | , '{event}: the event' 202 | , '{verb}: the verb (GET/POST/etc.)' 203 | , '{id}: the identifier' 204 | , '{idparent}: the parent object' 205 | , '{category}: the category' 206 | , '{subcategory}: the subcategory' 207 | , '{subsubcategory}: the subsubcategory']) 208 | ->setDefault($v[0]) 209 | ->evalParam(true); 210 | $tmp3 = $this->cli->createOrReplaceParam('extracolumn_sql2') 211 | //->setAllowEmpty() 212 | ->setInput() 213 | ->setDescription('', 'Select the namespace', ['example: eftec\\controller']) 214 | ->setDefault($v[1]) 215 | ->evalParam(true); 216 | $this->paths[$tmp->valueKey] = $tmp2->value . ', ' . $tmp3->value; 217 | } 218 | break; 219 | } 220 | } 221 | $this->cli->callVariablesCallBack(); 222 | $this->cli->downLevel(2); 223 | } 224 | 225 | /** @noinspection PhpUnused */ 226 | /** 227 | * @throws JsonException 228 | */ 229 | public function menuRouterOneSave(): void 230 | { 231 | $this->cli->upLevel('save'); 232 | $this->cli->setColor(['byellow'])->showBread(); 233 | $sg = $this->cli->createParam('yn', [], 'none') 234 | ->setDescription('', 'Do you want to save the configurations of connection?') 235 | ->setInput(true, 'optionshort', ['yes', 'no']) 236 | ->setDefault('yes') 237 | ->evalParam(true); 238 | if ($sg->value === 'yes') { 239 | $saveconfig = $this->cli->getParameter('routerfilename')->setInput()->evalParam(true); 240 | if ($saveconfig->value) { 241 | $r = $this->cli->saveDataPHPFormat($this->cli->getValue('routerfilename'), $this->getConfig()); 242 | if ($r === '') { 243 | $this->cli->showCheck('OK', 'green', 'file saved correctly'); 244 | } else { 245 | $this->cli->showCheck('ERROR', 'red', 'unable to save file :' . $r); 246 | } 247 | } 248 | } 249 | $this->cli->downLevel(); 250 | } 251 | 252 | /** @noinspection PhpUnused */ 253 | public function menuRouterOneHtaccess(): void 254 | { 255 | $file = 'index.php'; 256 | $content = $this->openTemplate(__DIR__ . '/templates/htaccess_template.php'); 257 | $content = str_replace('changeme.php', $file, $content); 258 | $this->validateWriteFile('.htaccess', $content); 259 | } 260 | 261 | public function menuRouterOneRouter(): void 262 | { 263 | $config = $this->getConfig(); 264 | $file = 'index.php'; 265 | $content = "openTemplate(__DIR__ . '/templates/route_template.php'); 266 | $namespaces = []; 267 | $paths = []; 268 | foreach ($this->paths as $k => $v) { 269 | $part = explode(', ', $v); 270 | $namespaces[$k] = $part[1]; 271 | $paths[$k] = $part[0]; 272 | } 273 | $content = str_replace([ 274 | '{{baseurldev}}', '{{baseurlprod}}', '{{dev}}', '{{namespaces}}', '{{paths}}' 275 | ], 276 | [ 277 | $config['baseurldev'], $config['baseurlprod'], $config['dev'], var_export($namespaces, true), var_export($paths, true) 278 | ], $content); 279 | $this->validateWriteFile($file, $content); 280 | } 281 | 282 | public function validateWriteFile(string $file, string $content): bool 283 | { 284 | $fail = false; 285 | $exists = @file_exists(getcwd() . '/' . $file); 286 | if ($exists) { 287 | $this->cli->showCheck('warning', 'yellow', "$file file exists, skipping"); 288 | $fail = true; 289 | } else { 290 | $result = @file_put_contents(getcwd() . '/' . $file, $content); 291 | if (!$result) { 292 | $this->cli->showCheck('error', 'red', "Unable to write " . getcwd() . '/' . "$file file\n"); 293 | $fail = true; 294 | } else { 295 | $this->cli->showCheck('ok', 'green', "OK"); 296 | } 297 | } 298 | return $fail; 299 | } 300 | 301 | /** 302 | * @param $filename 303 | * @return false|string 304 | */ 305 | public function openTemplate($filename) 306 | { 307 | $template = @file_get_contents($filename); 308 | if ($template === false) { 309 | throw new RuntimeException("Unable to read template file $filename"); 310 | } 311 | // we delete and replace the first line. 312 | return substr($template, strpos($template, "\n") + 1); 313 | } 314 | 315 | /** @noinspection PhpUnused */ 316 | /** 317 | * @throws JsonException 318 | */ 319 | public function menuRouterOneload(): void 320 | { 321 | $this->cli->upLevel('load'); 322 | $this->cli->setColor(['byellow'])->showBread(); 323 | $routerFileName = $this->cli->getParameter('routerfilename') 324 | ->setInput() 325 | ->evalParam(true); 326 | $this->routerOneLoad($routerFileName); 327 | $this->cli->downLevel(); 328 | } 329 | 330 | public function routerOneLoad(CliOneParam $routerFileName): void 331 | { 332 | if ($routerFileName->value) { 333 | $r = $this->cli->readDataPHPFormat($this->cli->getValue('routerfilename')); 334 | if ($r !== null && $r[0] === true) { 335 | $this->cli->showCheck('OK', 'green', 'file read correctly'); 336 | $this->setConfig($r[1]); 337 | } else { 338 | $this->cli->showCheck('ERROR', 'red', 'unable to read file ' . 339 | $this->cli->getValue('routerfilename') . ", cause " . $r[1]); 340 | } 341 | } 342 | } 343 | 344 | /** 345 | * @throws JsonException 346 | */ 347 | public function menuRouterOneConfigure(): void 348 | { 349 | $this->cli->upLevel('configure'); 350 | $this->cli->setColor(['byellow'])->showBread(); 351 | /* $this->cli->createOrReplaceParam('routerfilename', [], 'onlyinput') 352 | ->setDescription('The router filename', 'Select the router filename', [ 353 | 'example: index.php']) 354 | ->setInput(true, 'string', 'index.php') 355 | ->setCurrentAsDefault() 356 | ->evalParam(true);*/ 357 | $this->cli->createOrReplaceParam('dev', [], 'none') 358 | ->setDefault(gethostname()) 359 | ->setCurrentAsDefault() 360 | ->setDescription('', "What is the name of your dev machine", [ 361 | 'Select the name of your dev machine', 362 | 'If you don\' know it, then select any information']) 363 | ->setInput() 364 | ->evalParam(true); 365 | $this->cli->createOrReplaceParam('baseurldev', [], 'none') 366 | ->setDefault('http://localhost') 367 | ->setDescription('the base url', 'Select the base url(dev)', 368 | ['Example: https://localhost'], 'baseurldev') 369 | ->setRequired(false) 370 | ->setCurrentAsDefault() 371 | ->setInput() 372 | ->evalParam(true); 373 | $this->cli->createOrReplaceParam('baseurlprod', [], 'none') 374 | ->setDefault('https://www.domain.dom') 375 | ->setDescription('the base url', 'Select the base url(prod)', 376 | ['Example: https://localhost'], 'baseurlprod') 377 | ->setRequired(false) 378 | ->setCurrentAsDefault() 379 | ->setInput() 380 | ->evalParam(true); 381 | $this->cli->callVariablesCallBack(); 382 | $this->cli->downLevel(2); 383 | } 384 | 385 | public function getConfig(): array 386 | { 387 | $r = $this->cli->getValueAsArray(['baseurldev', 'baseurlprod', 'dev']); 388 | $r['dev'] = $r['dev'] === 'yes' ? gethostname() : ''; 389 | $r['paths'] = $this->paths; 390 | return $r; 391 | } 392 | 393 | public function setConfig(array $array): void 394 | { 395 | $this->paths = $array['paths']; 396 | unset($array['paths']); 397 | $this->cli->setParamUsingArray($array, ['baseurldev', 'baseurlprod', 'dev']); 398 | $this->cli->callVariablesCallBack(); 399 | } 400 | 401 | /*** 402 | * It finds the vendor path (where composer is located). 403 | * @param string|null $initPath 404 | * @return string 405 | * 406 | */ 407 | public static function findVendorPath(?string $initPath = null): string 408 | { 409 | $initPath = $initPath ?: __DIR__; 410 | $prefix = ''; 411 | $defaultvendor = $initPath; 412 | // finding vendor 413 | for ($i = 0; $i < 8; $i++) { 414 | if (@file_exists("$initPath/{$prefix}vendor/autoload.php")) { 415 | $defaultvendor = "{$prefix}vendor"; 416 | break; 417 | } 418 | $prefix .= '../'; 419 | } 420 | return $defaultvendor; 421 | } 422 | 423 | /** 424 | * It gets a list of files filtered by extension. 425 | * @param string $path 426 | * @param string $extension . Example: ".php", "php" (it could generate false positives) 427 | * @return array 428 | */ 429 | protected function getFiles(string $path, string $extension): array 430 | { 431 | $scanned_directory = array_diff(scandir($path), ['..', '.']); 432 | $scanned2 = []; 433 | foreach ($scanned_directory as $k) { 434 | $fullname = pathinfo($k)['extension'] ?? ''; 435 | if ($this->str_ends_with($fullname, $extension)) { 436 | $scanned2[$k] = $k; 437 | } 438 | } 439 | return $scanned2; 440 | } 441 | 442 | /** 443 | * for PHP <8.0 compatibility 444 | * @param string $haystack 445 | * @param string $needle 446 | * @return bool 447 | * 448 | */ 449 | protected function str_ends_with(string $haystack, string $needle): bool 450 | { 451 | $needle_len = strlen($needle); 452 | $haystack_len = strlen($haystack); 453 | if ($haystack_len < $needle_len) { 454 | return false; 455 | } 456 | return ($needle_len === 0 || 0 === substr_compare($haystack, $needle, -$needle_len)); 457 | } 458 | } 459 | -------------------------------------------------------------------------------- /lib/routeonecli: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options -MultiViews -Indexes 5 | 6 | # based in Laravel .htaccess 7 | RewriteEngine On 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To router 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | #RewriteRule ^ changeme.php [L] 21 | RewriteRule ^(.*)$ changeme.php?req=$1 [L,QSA] 22 | 23 | -------------------------------------------------------------------------------- /lib/templates/route_template.php: -------------------------------------------------------------------------------- 1 | 2 | use eftec\routeone\RouteOne; 3 | 4 | include __DIR__ . "/vendor/autoload.php"; 5 | /** 6 | * Generate by RouteOne.php 7 | */ 8 | function configureRouteOne():RouteOne 9 | { 10 | if (gethostname() === '{{dev}}') { 11 | $baseurl= "{{baseurldev}}"; // dev url 12 | } else { 13 | $baseurl="{{baseurlprod}}"; // prod url 14 | } 15 | $routeNS={{namespaces}}; 16 | $routePath={{paths}}; 17 | $route = new RouteOne($baseurl); 18 | foreach ($routePath as $k => $v) { 19 | $route->addPath($v, $k); 20 | } 21 | $route->fetchPath(); 22 | /* todo: we could do some auth work here. 23 | if($auth===null && $route->controller=="login") { 24 | $route->redirect("xxxx"); 25 | } 26 | */ 27 | try { 28 | // it will be the class somenamespace\ControllerNameController::actionAction 29 | $found = false; 30 | foreach ($routeNS as $k => $namespace) { 31 | if ($route->currentPath === $k) { 32 | $found = true; 33 | $route->callObjectEx($namespace . "\{controller}Controller"); 34 | } 35 | } 36 | if (!$found) { 37 | http_response_code(404); 38 | die(1); 39 | } 40 | } catch (Exception $e) { 41 | echo $e->getMessage(); 42 | http_response_code($e->getCode()); 43 | die(1); 44 | } 45 | return $route; 46 | } 47 | configureRouteOne(); 48 | 49 | --------------------------------------------------------------------------------