├── .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 | [](https://packagist.org/packages/eftec/routeone)
5 | [](https://packagist.org/packages/eftec/routeone)
6 | []()
7 | []()
8 | []()
9 | []()
10 | []()
11 | []()
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 | 
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 | 
824 |
825 | Pending means that the operation is pending to do, or it requires something to configure.
826 |
827 | * enter **configure**
828 |
829 | 
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 | 
843 |
844 | * And it will show the next menu, add, remove or edit.
845 | * Enter **add**
846 |
847 | 
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 | 
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 | *
116 | * - none:if the path uses a module then the type is calculated normally (default)
117 | * - modulefront:if the path uses a module then the type is front. If it doesn't use a module
118 | * then it is a controller, api or ws
119 | * - nomodulefront:if the path uses a module then the type is controller, api or ws.
120 | * If it doesn't use module then it is front
121 | *
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 | *
150 | * - none:if the path uses a module then the type is calculated
151 | * normally (default)
152 | * - modulefront:if the path uses a module then the type is
153 | * front. If it doesn't use a module then it is a controller, api or
154 | * ws
155 | * - nomodulefront:if the path uses a module then the type is
156 | * controller, api or ws. If it doesn't use module then it is
157 | * front
158 | *
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 | *
283 | * - {controller}: The controller (class) to call
284 | * - {action}: The action (method) to call
285 | * - {type}: The type (value)
286 | * - {module}: The module (value)
287 | * - {id}: The id (value)
288 | * - {idparent}: The id parent (value)
289 | * - {category}: The category (value)
290 | * - {subcategory}: The subcategory (value)
291 | * - {subsubcategory}: The subsubcategory (value)
292 | *
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 | *
435 | * - If the type is not frontend:
436 | * - api:. Expected path: api/Controller/action/id/idparent?_event=xx&extra=xxx
437 | * - ws:. Expected path: ws/Controller/action/id/idparent?_event=xx&extra=xxx
438 | * - controller:. Expected path: Controller/action/id/idparent?_event=xx&extra=xxx
439 | * - controller (using module):. Module/Controller/action/id/idparent?_event=xx&extra=xxx
440 | * - api (using module):. Module/api/ControllerApi/action/id/idparent/?_event=xx&extra=xxx
441 | * - ws (using module):. Module/ws/ControllerWS/action/id/idparent/?_event=xx&extra=xxx
442 | * - If the type is frontend:
443 | * - frontend:. Expected path: category/subcategory/subsubcategory/id/idparent?_event=xx&extra=xxx
444 | * - frontend (using module):.
445 | * Module/category/subcategory/subsubcategory/id/idparent?_event=xx&extra=xxx
446 | *
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 | *
546 | * - somedomain.dom/Controller1 is accepted
547 | * - somedomain.dom/controller1 is also accepted (and controller is equals as "Controller1")
548 | *
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 | *
842 | * - {controller} The name of the controller. Example:/web/controller/action
843 | * - {action} The current action. Example:/web/controller/action
844 | * - {verb} The current verb (GET, POST,PUT or DELETE). Example:[GET] /web/controller/action
845 | * The verb is ucfirst (Get instead of GET)
846 | * - {event} The current event. Example:/web/controller/action?_event=click
847 | * - {type} The current type of path (ws,controller,front,api)
848 | * - {module} The current module (if module is active). Example:/web/module1/controller/action
849 | *
850 | * - {id} The current id. Example:/web/controller/action/20
851 | * - {idparent} The current idparent. Example:/web/controller/action/10/20
852 | * - {category} The current category (type of path is front). Example: /web/food/fruit/season
853 | * - {subcategory} The current subcategory (type of path is front). Example:
854 | * /web/food/fruit/season
855 | * - {subsubcategory} The current subsubcategory (type of path is front). Example:
856 | * /web/food/fruit/season
857 | *
858 | * Note: You can also convert the case
859 | *
860 | * - {uc_*tag*} uppercase first
861 | * - {lc_*tag*} lowercase first
862 | * - {u_*tag*} uppercase
863 | * - {l_*tag*} lowercase
864 | *
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 | *
910 | * - ['id','idparent'] (positional argument)
911 | * - ['named'=>'id'] (named argument)
912 | * - ['named'=>'get:id:default'] get=origin, id:name,default(opt)
913 | * the default value
914 | *
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 | *
1783 | * - none:if the path uses a module then the type is calculated normally (default)
1784 | * - modulefront:if the path uses a module then the type is front. If it doesn't use a module
1785 | * then it is a controller, api or ws
1786 | * - nomodulefront:if the path uses a module then the type is controller, api or ws.
1787 | * If it doesn't use module then it is front
1788 | *
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 |
--------------------------------------------------------------------------------